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

[feat] add an ssr parameter to resolve for better skipping of SSR #2804

Merged
merged 15 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/spicy-moose-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Remove `config.kit.ssr` and `export const ssr` in favour of `ssr` parameter for `resolve` function in `handle`
27 changes: 24 additions & 3 deletions documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ export interface Response {
body?: StrictBody;
}

export interface ResolveOpts {
ssr?: boolean;
}

export interface Handle<Locals = Record<string, any>, Body = unknown> {
(input: {
request: Request<Locals, Body>;
resolve(request: Request<Locals, Body>): Response | Promise<Response>;
}): Response | Promise<Response>;
request: ServerRequest<Locals, Body>;
resolve(request: ServerRequest<Locals, Body>, opts?: ResolveOpts): MaybePromise<ServerResponse>;
}): MaybePromise<ServerResponse>;
}
```

Expand All @@ -76,6 +80,23 @@ export async function handle({ request, resolve }) {

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

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

```js
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, resolve }) {
const response = await resolve(request, {
ssr: !request.path.startsWith('/admin')
});

return response;
}
```

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

### handleError

If an error is thrown during rendering, this function will be called with the `error` and the `request` that caused it. This allows you to send data to an error tracking service, or to customise the formatting before printing the error to the console.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
---
title: SSR and JavaScript
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. Each setting can be controlled independently, but `ssr` and `hydrate` cannot both be `false` since that would result in nothing being rendered at all.

### ssr

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 [the discussion in the appendix](#appendix-ssr). Consider whether it's truly appropriate to disable and don't simply disable SSR because you've hit an issue with it.

You can disable SSR app-wide with the [`ssr` config option](#configuration-ssr), or a page-level `ssr` export:

```html
<script context="module">
export const ssr = false;
</script>
```
If both are specified, per-page settings override per-app settings in case of conflicts.

### router

Expand All @@ -46,7 +32,7 @@ Ordinarily, SvelteKit [hydrates](#appendix-hydration) your server-rendered HTML
</script>
```

> If `hydrate` and `router` are both `false`, SvelteKit will not add any JavaScript to the page at all.
> 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.

### 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 = {
register: true,
files: (filepath) => !/\.DS_STORE/.test(filepath)
},
ssr: true,
target: null,
trailingSlash: 'never',
vite: () => ({})
Expand Down Expand Up @@ -217,10 +216,6 @@ An object containing zero or more of the following values:

- `files` - a function with the type of `(filepath: string) => boolean`. When `true`, the given file will be available in `$service-worker.files`, otherwise it will be excluded.

### ssr

Enables or disables [server-side rendering](#ssr-and-javascript-ssr) app-wide.

### target

Specifies an element to mount the app to. It must be a DOM selector that identifies an element that exists in your template file. If unspecified, the app will be mounted to `document.body`.
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 @@ -78,7 +78,6 @@ export class App {
root,
service_worker: ${has_service_worker ? "'/service-worker.js'" : 'null'},
router: ${s(config.kit.router)},
ssr: ${s(config.kit.ssr)},
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 @@ -57,7 +57,7 @@ test('fills in defaults', () => {
},
protocol: null,
router: true,
ssr: true,
ssr: null,
target: null,
trailingSlash: 'never'
}
Expand Down Expand Up @@ -164,7 +164,7 @@ test('fills in partial blanks', () => {
},
protocol: null,
router: true,
ssr: true,
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 @@ -171,7 +171,14 @@ const options = object(
files: fun((filename) => !/\.DS_STORE/.test(filename))
}),

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

target: string(null),

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 @@ -56,7 +56,7 @@ test('load default config (esm)', async () => {
},
protocol: null,
router: true,
ssr: true,
ssr: null,
target: null,
trailingSlash: 'never'
}
Expand Down
1 change: 0 additions & 1 deletion packages/kit/src/core/dev/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ export async function create_plugin(config, output, cwd) {
read: (file) => fs.readFileSync(path.join(config.kit.files.assets, file)),
root,
router: config.kit.router,
ssr: config.kit.ssr,
target: config.kit.target,
template: ({ head, body, assets }) => {
let rendered = load_template(cwd, config)
Expand Down
10 changes: 4 additions & 6 deletions packages/kit/src/runtime/app/stores.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { getContext } from 'svelte';
import { browser } from './env.js';

const ssr = !browser;

// TODO remove this (for 1.0? after 1.0?)
let warned = false;
export function stores() {
Expand Down Expand Up @@ -58,9 +56,9 @@ export const navigating = {
/** @param {string} verb */
const throw_error = (verb) => {
throw new Error(
ssr
? `Can only ${verb} session store in browser`
: `Cannot ${verb} session store before subscribing`
browser
? `Cannot ${verb} session store before subscribing`
: `Can only ${verb} session store in browser`
);
};

Expand All @@ -69,7 +67,7 @@ export const session = {
subscribe(fn) {
const store = getStores().session;

if (!ssr) {
if (browser) {
session.set = store.set;
session.update = store.update;
}
Expand Down
19 changes: 13 additions & 6 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,25 @@ export async function respond(incoming, options, state = {}) {
print_error('path', 'pathname');
print_error('query', 'searchParams');

let ssr = true;

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

if (state.prerender && state.prerender.fallback) {
return await render_response({
url: request.url,
params: request.params,
options,
$session: await options.hooks.getSession(request),
page_config: { router: true, hydrate: true },
stuff: {},
page_config: { ssr: false, router: true, hydrate: true },
status: 200,
branch: []
branch: [],
ssr: false
});
}

Expand All @@ -86,7 +91,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);
: await render_page(request, route, match, options, state, ssr);

if (response) {
// inject ETags for 200 responses
Expand Down Expand Up @@ -126,7 +131,8 @@ export async function respond(incoming, options, state = {}) {
state,
$session,
status: 404,
error: new Error(`Not found: ${request.url.pathname}`)
error: new Error(`Not found: ${request.url.pathname}`),
ssr
});
}
}
Expand All @@ -144,7 +150,8 @@ export async function respond(incoming, options, state = {}) {
state,
$session,
status: 500,
error
error,
ssr
});
} catch (/** @type {unknown} */ e) {
const error = coalesce_to_error(e);
Expand Down
6 changes: 4 additions & 2 deletions packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +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
* @returns {Promise<import('types/hooks').ServerResponse | undefined>}
*/
export async function render_page(request, route, match, options, state) {
export async function render_page(request, route, match, options, state, ssr) {
if (state.initiator === route) {
// infinite request cycle detected
return {
Expand All @@ -29,7 +30,8 @@ export async function render_page(request, route, match, options, state) {
state,
$session,
route,
params
params,
ssr
});

if (response) {
Expand Down
14 changes: 8 additions & 6 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { s } from '../../../utils/misc.js';
* branch: Array<import('./types').Loaded>;
* options: import('types/internal').SSRRenderOptions;
* $session: any;
* page_config: { hydrate: boolean, router: boolean, ssr: boolean };
* page_config: { hydrate: boolean, router: boolean };
* status: number;
* error?: Error,
* error?: Error;
* url: URL;
* params: Record<string, string>
* params: Record<string, string>;
* ssr: boolean;
* stuff: Record<string, any>;
* }} opts
*/
Expand All @@ -29,6 +30,7 @@ export async function render_response({
error,
url,
params,
ssr,
stuff
}) {
const css = new Set(options.manifest._.entry.css);
Expand All @@ -47,7 +49,7 @@ export async function render_response({
error.stack = options.get_stack(error);
}

if (page_config.ssr) {
if (ssr) {
branch.forEach(({ node, loaded, fetched, uses_credentials }) => {
if (node.css) node.css.forEach((url) => css.add(url));
if (node.js) node.js.forEach((url) => js.add(url));
Expand Down Expand Up @@ -150,9 +152,9 @@ export async function render_response({
throw new Error(`Failed to serialize session data: ${error.message}`);
})},
route: ${!!page_config.router},
spa: ${!page_config.ssr},
spa: ${!ssr},
trailing_slash: ${s(options.trailing_slash)},
hydrate: ${page_config.ssr && page_config.hydrate ? `{
hydrate: ${ssr && page_config.hydrate ? `{
status: ${status},
error: ${serialize_error(error)},
nodes: [
Expand Down
Loading