diff --git a/.changeset/rare-eels-run.md b/.changeset/rare-eels-run.md new file mode 100644 index 000000000000..469a9efaed60 --- /dev/null +++ b/.changeset/rare-eels-run.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[breaking] move `hydrate` and `router` options to `handle` diff --git a/documentation/docs/04-hooks.md b/documentation/docs/04-hooks.md index 7a3dcec0d897..4afc266a1950 100644 --- a/documentation/docs/04-hooks.md +++ b/documentation/docs/04-hooks.md @@ -48,6 +48,8 @@ export interface Response { } export interface ResolveOpts { + hydrate?: boolean; + router?: boolean; ssr?: boolean; } @@ -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} */ @@ -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 diff --git a/documentation/docs/11-page-options.md b/documentation/docs/11-page-options.md index 82a95eee0e82..5bc26e7608c7 100644 --- a/documentation/docs/11-page-options.md +++ b/documentation/docs/11-page-options.md @@ -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 - -``` - -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 - -``` - -> 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 diff --git a/documentation/docs/14-configuration.md b/documentation/docs/14-configuration.md index d0f4b010068e..be442bb563e8 100644 --- a/documentation/docs/14-configuration.md +++ b/documentation/docs/14-configuration.md @@ -56,7 +56,6 @@ const config = { onError: 'fail' }, protocol: null, - router: true, serviceWorker: { register: true, files: (filepath) => !/\.DS_STORE/.test(filepath) @@ -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: diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index 86a1f3c35dd5..c6f5a6a9e718 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -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)} diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 636163b9f41d..ea53ebbdcc6d 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -61,7 +61,7 @@ test('fills in defaults', () => { pages: undefined }, protocol: null, - router: true, + router: null, ssr: null, target: null, trailingSlash: 'never' @@ -173,7 +173,7 @@ test('fills in partial blanks', () => { pages: undefined }, protocol: null, - router: true, + router: null, ssr: null, target: null, trailingSlash: 'never' diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 0eaf5e8567e6..74f12bc258d0 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -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), diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js index c85d2e60c2ed..f419984a7736 100644 --- a/packages/kit/src/core/config/test/index.js +++ b/packages/kit/src/core/config/test/index.js @@ -60,7 +60,7 @@ test('load default config (esm)', async () => { pages: undefined }, protocol: null, - router: true, + router: null, ssr: null, target: null, trailingSlash: 'never' diff --git a/packages/kit/src/runtime/client/renderer.js b/packages/kit/src/runtime/client/renderer.js index 5a5856d5a5d8..7e47decf45a2 100644 --- a/packages/kit/src/runtime/client/renderer.js +++ b/packages/kit/src/runtime/client/renderer.js @@ -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(); - } } /** diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index ef6c5981099d..838c3abad51a 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -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'); @@ -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; @@ -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); + } + }); }); } @@ -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} diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 9692866bbfa7..09a2f83601fb 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -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({ @@ -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 + } }); } @@ -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 @@ -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 }); } } @@ -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); diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 1359cdb0eadb..ea17f5caf71c 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -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} resolve_opts * @returns {Promise} */ -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 { @@ -31,7 +31,7 @@ export async function render_page(request, route, match, options, state, ssr) { $session, route, params, - ssr + resolve_opts }); if (response) { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index db66d9949841..d35042845dc6 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -14,12 +14,11 @@ import { create_prerendering_url_proxy } from './utils.js'; * options: import('types/internal').SSRRenderOptions; * state: import('types/internal').SSRRenderState; * $session: any; - * page_config: { hydrate: boolean, router: boolean }; * status: number; * error?: Error; * url: URL; * params: Record; - * ssr: boolean; + * resolve_opts: Required; * stuff: Record; * }} opts */ @@ -28,14 +27,14 @@ export async function render_response({ options, state, $session, - page_config, status, error, url, params, - ssr, + resolve_opts, stuff }) { + const { hydrate, router, ssr } = resolve_opts; const css = new Set(options.manifest._.entry.css); const js = new Set(options.manifest._.entry.js); /** @type {Map} */ @@ -60,7 +59,7 @@ export async function render_response({ if (node.styles) Object.entries(node.styles).forEach(([k, v]) => styles.set(k, v)); // TODO probably better if `fetched` wasn't populated unless `hydrate` - if (fetched && page_config.hydrate) serialized_data.push(...fetched); + if (fetched && hydrate) serialized_data.push(...fetched); if (uses_credentials) is_private = true; @@ -151,7 +150,7 @@ export async function render_response({ .map((dep) => `\n\t`) .join(''); - if (page_config.router || page_config.hydrate) { + if (router || hydrate) { head += Array.from(js) .map((dep) => `\n\t`) .join(''); @@ -165,10 +164,10 @@ export async function render_response({ session: ${try_serialize($session, (error) => { throw new Error(`Failed to serialize session data: ${error.message}`); })}, - route: ${!!page_config.router}, + route: ${!!router}, spa: ${!ssr}, trailing_slash: ${s(options.trailing_slash)}, - hydrate: ${ssr && page_config.hydrate ? `{ + hydrate: ${ssr && hydrate ? `{ status: ${status}, error: ${serialize_error(error)}, nodes: [ diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index c38fb7264365..b1b5ba2bbbf2 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -19,24 +19,20 @@ import { coalesce_to_error } from '../../../utils/error.js'; * $session: any; * route: import('types/internal').SSRPage; * params: Record; - * ssr: boolean; + * resolve_opts: Required * }} opts * @returns {Promise} */ export async function respond(opts) { - const { request, options, state, $session, route, ssr } = opts; + const { request, options, state, $session, route, resolve_opts } = opts; /** @type {Array} */ let nodes; - if (!ssr) { + if (!resolve_opts.ssr) { return await render_response({ ...opts, branch: [], - page_config: { - hydrate: true, - router: true - }, status: 200, url: request.url, stuff: {} @@ -59,15 +55,13 @@ export async function respond(opts) { $session, status: 500, error, - ssr + resolve_opts }); } // the leaf node will be present. only layouts may be undefined const leaf = /** @type {SSRNode} */ (nodes[nodes.length - 1]).module; - let page_config = get_page_config(leaf, options); - if (!leaf.prerender && state.prerender && !state.prerender.all) { // if the page has `export const prerender = true`, continue, // otherwise bail out at this point @@ -91,7 +85,7 @@ export async function respond(opts) { let stuff = {}; - ssr: if (ssr) { + ssr: if (resolve_opts.ssr) { for (let i = 0; i < nodes.length; i += 1) { const node = nodes[i]; @@ -169,7 +163,6 @@ export async function respond(opts) { continue; } - page_config = get_page_config(error_node.module, options); branch = branch.slice(0, j + 1).concat(error_loaded); stuff = { ...node_loaded.stuff, ...error_loaded.stuff }; break ssr; @@ -194,7 +187,7 @@ export async function respond(opts) { $session, status, error, - ssr + resolve_opts }), set_cookie_headers ); @@ -216,7 +209,6 @@ export async function respond(opts) { ...opts, stuff, url: request.url, - page_config, status, error, branch: branch.filter(Boolean) @@ -239,24 +231,6 @@ export async function respond(opts) { } } -/** - * @param {import('types/internal').SSRComponent} leaf - * @param {SSRRenderOptions} options - */ -function get_page_config(leaf, options) { - // TODO remove for 1.0 - if ('ssr' in leaf) { - throw new Error( - '`export const ssr` has been removed — use the handle hook instead: https://kit.svelte.dev/docs#hooks-handle' - ); - } - - return { - router: 'router' in leaf ? !!leaf.router : options.router, - hydrate: 'hydrate' in leaf ? !!leaf.hydrate : options.hydrate - }; -} - /** * @param {ServerResponse} response * @param {string[]} set_cookie_headers 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 cfd05eec5dc4..87267476714d 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -16,7 +16,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; * $session: any; * status: number; * error: Error; - * ssr: boolean; + * resolve_opts: Required * }} opts */ export async function respond_with_error({ @@ -26,7 +26,7 @@ export async function respond_with_error({ $session, status, error, - ssr + resolve_opts }) { try { const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout @@ -71,17 +71,13 @@ export async function respond_with_error({ options, state, $session, - page_config: { - hydrate: options.hydrate, - router: options.router - }, stuff: error_loaded.stuff, status, error, branch: [layout_loaded, error_loaded], url: request.url, params, - ssr + resolve_opts }); } catch (err) { const error = coalesce_to_error(err); diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js index d82a0f8379bc..b4581753e6b6 100644 --- a/packages/kit/test/apps/basics/src/hooks.js +++ b/packages/kit/test/apps/basics/src/hooks.js @@ -31,11 +31,16 @@ export const handle = sequence( return resolve(request); }, async ({ request, resolve }) => { - if (request.url.pathname === '/errors/error-in-handle') { + const pathname = request.url.pathname; + if (pathname === '/errors/error-in-handle') { throw new Error('Error in handle'); } - const response = await resolve(request, { ssr: !request.url.pathname.startsWith('/no-ssr') }); + const response = await resolve(request, { + hydrate: pathname !== '/no-hydrate' && pathname !== '/no-hydrate/no-js', + router: pathname !== '/no-router/a' && pathname !== '/no-hydrate/no-js', + ssr: !request.url.pathname.startsWith('/no-ssr') + }); return { ...response, diff --git a/packages/kit/test/apps/basics/src/routes/no-hydrate/index.svelte b/packages/kit/test/apps/basics/src/routes/no-hydrate/index.svelte index 966b9c417a35..87691962dc42 100644 --- a/packages/kit/test/apps/basics/src/routes/no-hydrate/index.svelte +++ b/packages/kit/test/apps/basics/src/routes/no-hydrate/index.svelte @@ -1,6 +1,4 @@ - -

look ma no javascript

\ No newline at end of file +

look ma no javascript

diff --git a/packages/kit/test/apps/basics/src/routes/no-router/__layout.svelte b/packages/kit/test/apps/basics/src/routes/no-router/__layout.svelte index 918af78c5965..bd7db22d6e0f 100644 --- a/packages/kit/test/apps/basics/src/routes/no-router/__layout.svelte +++ b/packages/kit/test/apps/basics/src/routes/no-router/__layout.svelte @@ -7,6 +7,6 @@ a -b +b - \ No newline at end of file + diff --git a/packages/kit/test/apps/basics/src/routes/no-router/a.svelte b/packages/kit/test/apps/basics/src/routes/no-router/a.svelte index 32bd0fc793a0..57f9d40797b8 100644 --- a/packages/kit/test/apps/basics/src/routes/no-router/a.svelte +++ b/packages/kit/test/apps/basics/src/routes/no-router/a.svelte @@ -1,5 +1 @@ - - -

a

\ No newline at end of file +

a

diff --git a/packages/kit/test/apps/basics/src/routes/no-router/b.svelte b/packages/kit/test/apps/basics/src/routes/no-router/b.svelte index 51902b4da7a6..e96ed3db8987 100644 --- a/packages/kit/test/apps/basics/src/routes/no-router/b.svelte +++ b/packages/kit/test/apps/basics/src/routes/no-router/b.svelte @@ -1 +1 @@ -

b

\ No newline at end of file +

b

diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 469ebe3d9eeb..ce9b120bc200 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1104,7 +1104,7 @@ test.describe.parallel('Nested layouts', () => { }); }); -test.describe.parallel('Page options', () => { +test.describe.parallel('Resolve options', () => { test('does not hydrate page with hydrate=false', async ({ page, javaScriptEnabled }) => { await page.goto('/no-hydrate'); diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index 3dc01a6afca5..72d13a7b35f9 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -25,6 +25,8 @@ export interface GetSession, Body = unknown, Sessio export interface ResolveOpts { ssr?: boolean; + hydrate?: boolean; + router?: boolean; } export interface Handle, Body = unknown> { diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 2d34d85ad43d..ee3d3f1cd2b5 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -44,8 +44,6 @@ export interface Logger { } export interface SSRComponent { - router?: boolean; - hydrate?: boolean; prerender?: boolean; load: Load; default: {