Skip to content

Commit

Permalink
[feat] More powerful and configurable rendering options (#2008)
Browse files Browse the repository at this point in the history
  • Loading branch information
benmccann authored Jul 27, 2021
1 parent f732275 commit 538de3e
Show file tree
Hide file tree
Showing 18 changed files with 191 additions and 100 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-hornets-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[feat] More powerful and configurable rendering options
10 changes: 6 additions & 4 deletions documentation/docs/11-ssr-and-javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ By default, SvelteKit will render any component first on the server and send it

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.
The app-wide config options take a function, which lets you set configure the option in an advanced manner on a per-request and per-page basis. E.g. you could disable SSR for `/admin` or enable SSR only for search engine crawlers (aka [dynamic rendering](https://developers.google.com/search/docs/advanced/javascript/dynamic-rendering)).

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).
Disabling [server-side rendering](#appendix-ssr) effectively turns your SvelteKit app into a [**single-page app** or SPA](#appendix-csr-and-spa). The default app-wide config option value is a function which reads the page value. Reading the page value causes the page to be loaded on the server. If you'd like to avoid this because you're building a SPA, you will need to set a value such as a boolean for each of the four rendering options which does not access the page-level settings.

> 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:
SSR can be configured with app-wide [`ssr` config option](#configuration-ssr), or a page-level `ssr` export:

```html
<script context="module">
Expand All @@ -26,7 +28,7 @@ You can disable SSR app-wide with the [`ssr` config option](#configuration-ssr),

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:
In certain circumstances you might need to configure [client-side routing](#appendix-routing) with the app-wide [`router` config option](#configuration-router) or the page-level `router` export:

```html
<script context="module">
Expand Down
21 changes: 17 additions & 4 deletions documentation/docs/14-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ const config = {
floc: false,
host: null,
hostHeader: null,
hydrate: true,
// hydrate unless disabled on page
hydrate: async ({ page }) => {
const leaf = await page;
return 'hydrate' in leaf ? !!leaf.hydrate : true;
},
package: {
dir: 'package',
emitTypes: true,
Expand All @@ -47,15 +51,24 @@ const config = {
},
prerender: {
crawl: true,
enabled: true,
// don't prerender unless enabled on page
enabled: expect_page_scriptable(async ({ page }) => !!(await page).prerender),
onError: 'fail',
pages: ['*']
},
router: true,
// route unless disabled on page
router: async ({ page }) => {
const leaf = await page;
return 'router' in leaf ? !!leaf.router : true;
},
serviceWorker: {
exclude: []
},
ssr: true,
// do SSR unless disabled on page
ssr: async ({ page }) => {
const leaf = await page;
return 'ssr' in leaf ? !!leaf.ssr : true;
},
target: null,
trailingSlash: 'never',
vite: () => ({})
Expand Down
13 changes: 5 additions & 8 deletions documentation/faq/80-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Please use SDK v9 which provides a modular SDK approach that's currently in beta

### How do I use a client-side only library that depends on `document` or `window`?

Vite will attempt to process all imported libraries and may fail when encountering a library that is not compatible with SSR. [This currently occurs even when SSR is disabled](https://github.com/sveltejs/kit/issues/754).
Vite will attempt to process all imported libraries and may fail when encountering a library that is not compatible with SSR. This occurs even when SSR is disabled unless you also override the default values for `hydrate`, `router`, and `prerender.enabled` [as mentioned in the docs](docs#ssr-and-javascript-ssr).

If you need access to the `document` or `window` variables or otherwise need it to run only on the client-side you can wrap it in a `browser` check:

Expand All @@ -30,20 +30,17 @@ if (browser) {
}
```

You can also run code in `onMount` if you'd like to run it after the component has been first rendered to the DOM. In this case, you may still find a benefit of including a `browser` check as shown below because Vite may otherwise attempt to optimize the dependency and fail on it. [We hope to make this unnecessary in the future](https://github.com/sveltejs/svelte/issues/6372).
You can also run code in `onMount` if you'd like to run it after the component has been first rendered to the DOM.

```html
<script>
import { onMount } from 'svelte';
import { browser } from '$app/env';
let lib;
if (browser) {
onMount(async () => {
lib = (await import('some-browser-only-library')).default;
});
}
onMount(async () => {
lib = (await import('some-browser-only-library')).default;
});
</script>
```

Expand Down
6 changes: 3 additions & 3 deletions packages/adapter-static/test/apps/prerendered/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"name": "~TODO~",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"start": "svelte-kit start"
"dev": "../../../../kit/svelte-kit.js dev",
"build": "../../../../kit/svelte-kit.js build",
"start": "../../../../kit/svelte-kit.js start"
},
"devDependencies": {
"@sveltejs/kit": "next",
Expand Down
6 changes: 3 additions & 3 deletions packages/adapter-static/test/apps/spa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"name": "~TODO~",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"start": "svelte-kit start"
"dev": "../../../../kit/svelte-kit.js dev",
"build": "../../../../kit/svelte-kit.js build",
"start": "../../../../kit/svelte-kit.js start"
},
"devDependencies": {
"@sveltejs/adapter-node": "next",
Expand Down
7 changes: 4 additions & 3 deletions packages/kit/src/core/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,16 +332,17 @@ async function build_server(
error.stack = options.get_stack(error);
},
hooks: get_hooks(user_hooks),
hydrate: ${s(config.kit.hydrate)},
hydrate: ${config.kit.hydrate},
initiator: undefined,
load_component,
manifest,
paths: settings.paths,
prerender: ${config.kit.prerender && config.kit.prerender.enabled},
read: settings.read,
root,
service_worker: ${service_worker_entry_file ? "'/service-worker.js'" : 'null'},
router: ${s(config.kit.router)},
ssr: ${s(config.kit.ssr)},
router: ${config.kit.router},
ssr: ${config.kit.ssr},
target: ${s(config.kit.target)},
template,
trailing_slash: ${s(config.kit.trailingSlash)}
Expand Down
29 changes: 16 additions & 13 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { deep_merge, validate_config } from './index.js';

test('fills in defaults', () => {
const validated = validate_config({});

// @ts-expect-error
delete validated.kit.vite;
delete_complex_opts(validated);

assert.equal(validated, {
compilerOptions: null,
Expand All @@ -27,7 +25,6 @@ test('fills in defaults', () => {
floc: false,
host: null,
hostHeader: null,
hydrate: true,
package: {
dir: 'package',
exports: {
Expand All @@ -49,14 +46,11 @@ test('fills in defaults', () => {
},
prerender: {
crawl: true,
enabled: true,
// TODO: remove this for the 1.0 release
force: undefined,
onError: 'fail',
pages: ['*']
},
router: true,
ssr: true,
target: null,
trailingSlash: 'never'
},
Expand Down Expand Up @@ -107,8 +101,7 @@ test('fills in partial blanks', () => {

assert.equal(validated.kit.vite(), {});

// @ts-expect-error
delete validated.kit.vite;
delete_complex_opts(validated);

assert.equal(validated, {
compilerOptions: null,
Expand All @@ -129,7 +122,6 @@ test('fills in partial blanks', () => {
floc: false,
host: null,
hostHeader: null,
hydrate: true,
package: {
dir: 'package',
exports: {
Expand All @@ -151,14 +143,11 @@ test('fills in partial blanks', () => {
},
prerender: {
crawl: true,
enabled: true,
// TODO: remove this for the 1.0 release
force: undefined,
onError: 'fail',
pages: ['*']
},
router: true,
ssr: true,
target: null,
trailingSlash: 'never'
},
Expand Down Expand Up @@ -517,3 +506,17 @@ deepMergeSuite('merge including toString', () => {
});

deepMergeSuite.run();

/** @param {import('types/config').ValidatedConfig} validated */
function delete_complex_opts(validated) {
// @ts-expect-error
delete validated.kit.vite;
// @ts-expect-error
delete validated.kit.hydrate;
// @ts-expect-error
delete validated.kit.prerender.enabled;
// @ts-expect-error
delete validated.kit.router;
// @ts-expect-error
delete validated.kit.ssr;
}
34 changes: 30 additions & 4 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ const options = {

hostHeader: expect_string(null),

hydrate: expect_boolean(true),
hydrate: expect_page_scriptable(async ({ page }) => {
const leaf = await page;
return 'hydrate' in leaf ? !!leaf.hydrate : true;
}),
serviceWorker: {
type: 'branch',
children: {
Expand Down Expand Up @@ -120,7 +123,7 @@ const options = {
type: 'branch',
children: {
crawl: expect_boolean(true),
enabled: expect_boolean(true),
enabled: expect_page_scriptable(async ({ page }) => !!(await page).prerender),
// TODO: remove this for the 1.0 release
force: {
type: 'leaf',
Expand Down Expand Up @@ -170,9 +173,15 @@ const options = {
}
},

router: expect_boolean(true),
router: expect_page_scriptable(async ({ page }) => {
const leaf = await page;
return 'router' in leaf ? !!leaf.router : true;
}),

ssr: expect_boolean(true),
ssr: expect_page_scriptable(async ({ page }) => {
const leaf = await page;
return 'ssr' in leaf ? !!leaf.ssr : true;
}),

target: expect_string(null),

Expand Down Expand Up @@ -259,6 +268,23 @@ function expect_boolean(boolean) {
};
}

/**
* @param {import('types/config').ScriptablePageOpt<boolean>} value
* @returns {ConfigDefinition}
*/
function expect_page_scriptable(value) {
return {
type: 'leaf',
default: value,
validate: (option, keypath) => {
if (typeof option !== 'boolean' && typeof option !== 'function') {
throw new Error(`${keypath} should be a boolean or function that returns one`);
}
return option;
}
};
}

/**
* @param {string[]} options
* @returns {ConfigDefinition}
Expand Down
23 changes: 16 additions & 7 deletions packages/kit/src/core/config/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ async function testLoadDefaultConfig(path) {
const cwd = join(__dirname, 'fixtures', path);

const config = await load_config({ cwd });

// @ts-expect-error
delete config.kit.vite; // can't test equality of a function
delete_complex_opts(config);

assert.equal(config, {
compilerOptions: null,
Expand All @@ -37,7 +35,6 @@ async function testLoadDefaultConfig(path) {
floc: false,
host: null,
hostHeader: null,
hydrate: true,
package: {
dir: 'package',
exports: {
Expand All @@ -54,9 +51,7 @@ async function testLoadDefaultConfig(path) {
exclude: []
},
paths: { base: '', assets: '/.' },
prerender: { crawl: true, enabled: true, force: undefined, onError: 'fail', pages: ['*'] },
router: true,
ssr: true,
prerender: { crawl: true, force: undefined, onError: 'fail', pages: ['*'] },
target: null,
trailingSlash: 'never'
},
Expand Down Expand Up @@ -103,3 +98,17 @@ test('errors on loading config with incorrect default export', async () => {
});

test.run();

/** @param {import('types/config').ValidatedConfig} validated */
function delete_complex_opts(validated) {
// @ts-expect-error
delete validated.kit.vite;
// @ts-expect-error
delete validated.kit.hydrate;
// @ts-expect-error
delete validated.kit.prerender.enabled;
// @ts-expect-error
delete validated.kit.router;
// @ts-expect-error
delete validated.kit.ssr;
}
1 change: 1 addition & 0 deletions packages/kit/src/core/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ async function create_handler(vite, config, dir, cwd, manifest) {
};
},
manifest,
prerender: config.kit.prerender.enabled,
read: (file) => fs.readFileSync(path.join(config.kit.files.assets, file)),
root,
router: config.kit.router,
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function respond(incoming, options, state = {}) {
return await render_response({
options,
$session: await options.hooks.getSession(request),
page_config: { ssr: false, router: true, hydrate: true },
page_config: { ssr: false, router: true, hydrate: true, prerender: true },
status: 200,
branch: []
});
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const s = JSON.stringify;
* @param {{
* options: import('types/internal').SSRRenderOptions;
* $session: any;
* page_config: { hydrate: boolean, router: boolean, ssr: boolean };
* page_config: import('types/config').PageOpts;
* status: number;
* error?: Error,
* branch?: Array<import('./types').Loaded>;
Expand Down
Loading

0 comments on commit 538de3e

Please sign in to comment.