Skip to content
Closed
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/sweet-masks-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Added template transform api, requested in the sapper repo #1695 #1642
50 changes: 45 additions & 5 deletions documentation/docs/04-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
title: Setup
---

An optional `src/setup.js` (or `src/setup.ts`, or `src/setup/index.js`) file exports two functions that run on the server — **prepare** and **getSession**.
An optional `src/setup.js` (or `src/setup.ts`, or `src/setup/index.js`) file exports three functions that run on the server — **prepare**, **transformTemplate** and **getSession**.

Both functions, if provided, run for every page or endpoint request SvelteKit receives.
These functions, if provided, run for every page or endpoint request SvelteKit receives.

> The location of this file can be [configured](#configuration) as `config.kit.files.setup`

Expand Down Expand Up @@ -65,17 +65,19 @@ export async function prepare(incoming) {
headers['set-cookie'] = `session_id=${uuid()}; HttpOnly`;
}

const darkMode = cookies.darkMode || false;

return {
- headers
+ headers,
+ context: {
+ user: await db.get_user(cookies.session_id)
+ user: await db.get_user(cookies.session_id),
+ darkMode,
+ }
};
}
```


### getSession

This function takes the `context` returned from `prepare` and returns a `session` object that is safe to expose to the browser.
Expand All @@ -101,4 +103,42 @@ export function getSession({ context }) {
}
```

> `session` must be serializable, which means it must not contain things like functions or custom classes, just built-in JavaScript data types
> `session` must be serializable, which means it must not contain things like functions or custom classes, just built-in JavaScript data types

### transformTemplate

This function takes the `src/app.html template` and the `context` returned from `prepare`. It should return the template after transforming it.

```js
/**
* @param {{
* template: string
* context: any
* }} options
* @returns {string}
*/
export function transformTemplate({ context, template }) {
if (!context.darkMode) {
return template;
}

return template.replace('%My.HtmlClass%', 'dark');
}
```

> The corresponding `src/app.html` file would look like this:

```html
<!DOCTYPE html>
<html class="%My.HtmlClass%" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>
```
22 changes: 11 additions & 11 deletions examples/sandbox/src/app.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1">
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>
<html class="%svelte.htmlClass%" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>
10 changes: 9 additions & 1 deletion examples/sandbox/src/setup/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export function prepare({ headers }) {
return {
context: {
answer: 42
answer: 42,
darkMode: true
},
headers: {
'x-foo': 'banana'
Expand All @@ -12,3 +13,10 @@ export function prepare({ headers }) {
export function getSession({ context }) {
return context;
}

export function transformTemplate({ context, template }) {
if (context.darkMode) {
return template.replace('%svelte.htmlClass%', 'dark');
}
return template;
}
22 changes: 16 additions & 6 deletions packages/kit/src/runtime/server/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import { ssr } from './index.js';
* request: import('types.internal').Request;
* options: import('types.internal').SSRRenderOptions;
* $session: any;
* context: any;
* route: import('types.internal').SSRPage;
* status: number;
* error: Error
* }} opts
* @returns {Promise<import('types.internal').SKResponse>}
*/
async function get_response({ request, options, $session, route, status = 200, error }) {
async function get_response({ request, options, context, $session, route, status = 200, error }) {
const host = options.host || request.headers[options.host_header];

/** @type {Record<string, import('types.internal').SKResponse>} */
Expand Down Expand Up @@ -181,7 +182,7 @@ async function get_response({ request, options, $session, route, status = 200, e
const components = [];
const props_promises = [];

let context = {};
let loadContext = {};
let maxage;

if (options.only_render_prerenderable_pages) {
Expand Down Expand Up @@ -214,7 +215,7 @@ async function get_response({ request, options, $session, route, status = 200, e
return $session;
},
fetch: fetcher,
context: { ...context }
context: { ...loadContext }
});

if (!loaded) return;
Expand All @@ -241,6 +242,7 @@ async function get_response({ request, options, $session, route, status = 200, e
options,
$session,
route,
context,
status: loaded.status,
error: loaded.error
});
Expand All @@ -256,8 +258,8 @@ async function get_response({ request, options, $session, route, status = 200, e
}

if (loaded.context) {
context = {
...context,
loadContext = {
...loadContext,
...loaded.context
};
}
Expand Down Expand Up @@ -314,6 +316,7 @@ async function get_response({ request, options, $session, route, status = 200, e
request,
options,
$session,
context,
route,
status: 500,
error: e instanceof Error ? e : { name: 'Error', message: e.toString() }
Expand Down Expand Up @@ -393,10 +396,16 @@ async function get_response({ request, options, $session, route, status = 200, e
headers['cache-control'] = `${uses_credentials ? 'private' : 'public'}, max-age=${maxage}`;
}

let template = options.template({ head, body });

if (options.setup.transformTemplate) {
template = (await options.setup.transformTemplate({ template, context })) || template;
}

return {
status,
headers,
body: options.template({ head, body }),
body: template,
dependencies
};
}
Expand Down Expand Up @@ -424,6 +433,7 @@ export default async function render_page(request, route, context, options) {
request,
options,
$session,
context,
route,
status: route ? 200 : 404,
error: route ? null : new Error(`Not found: ${request.path}`)
Expand Down
11 changes: 11 additions & 0 deletions packages/kit/test/apps/template-transform/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html class="%svelte.htmlClass%" lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
</head>
<body>
%svelte.body%
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as assert from 'uvu/assert';

/** @type {import('../../../../types').TestMaker} */
export default function (test) {
test('Should apply dark class to html element', '/', async ({ page }) => {
assert.equal(await page.evaluate(() => document.documentElement.className), 'dark');
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Template Transform Test
28 changes: 28 additions & 0 deletions packages/kit/test/apps/template-transform/src/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export function prepare() {
return {
context: {
darkMode: true,
answer: 42
}
};
}

/** @param {any} context */
export function getSession({ context }) {
return context;
}

/**
* @param {{
* template: string
* context: any
* }} options
* @returns {string}
*/
export function transformTemplate({ context, template }) {
if (!context.darkMode) {
return template;
}

return template.replace('%svelte.htmlClass%', 'dark');
}
11 changes: 11 additions & 0 deletions packages/kit/test/apps/template-transform/svelte.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
kit: {
hostHeader: 'x-forwarded-host',
vite: {
build: {
minify: false
},
clearScreen: false
}
}
};
1 change: 1 addition & 0 deletions packages/kit/types.internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export type SSRRenderOptions = {
headers?: Headers;
};
getSession?: ({ context }: { context: any }) => any;
transformTemplate?: ({ context, template }: { context: any; template: string }) => string;
};
dev?: boolean;
amp?: boolean;
Expand Down