Skip to content
This repository was archived by the owner on Jan 11, 2023. It is now read-only.

Commit eac4a61

Browse files
Add template transform api
1 parent f4cecc4 commit eac4a61

File tree

15 files changed

+356
-7
lines changed

15 files changed

+356
-7
lines changed

.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"useTabs": true,
3+
"singleQuote": true
4+
}

runtime/src/server/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
export { default as middleware } from './middleware/index';
2+
export { registerTemplateTransformer } from './middleware/template_transform';
3+
export type {
4+
TransformData,
5+
Transformer
6+
} from './middleware/template_transform';

runtime/src/server/middleware/get_page_handler.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import App from '@sapper/internal/App.svelte';
1919
import { PageContext, PreloadResult } from '@sapper/common';
2020
import detectClientOnlyReferences from './detect_client_only_references';
21+
import { transformTemplate } from './template_transform';
2122

2223
export function get_page_handler(
2324
manifest: Manifest,
@@ -375,13 +376,10 @@ export function get_page_handler(
375376
styles = (css && css.code ? `<style${nonce_attr}>${css.code}</style>` : '');
376377
}
377378

378-
const body = template()
379-
.replace('%sapper.base%', () => `<base href="${req.baseUrl}/">`)
380-
.replace('%sapper.scripts%', () => `<script${nonce_attr}>${script}</script>`)
381-
.replace('%sapper.html%', () => html)
382-
.replace('%sapper.head%', () => head)
383-
.replace('%sapper.styles%', () => styles)
384-
.replace(/%sapper\.cspnonce%/g, () => nonce_value);
379+
const body = transformTemplate(
380+
template(),
381+
{req, nonce_attr, nonce_value, html, head,styles,script}
382+
);
385383

386384
res.statusCode = status;
387385
res.end(body);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { SapperRequest } from '@sapper/server';
2+
3+
export type TransformData = Readonly<{
4+
html: any;
5+
head: any;
6+
styles: string;
7+
script: string;
8+
nonce_value: string;
9+
nonce_attr: string;
10+
req: SapperRequest;
11+
}>;
12+
13+
export type Transformer = (body: string, data: TransformData) => string;
14+
15+
const transformers: Transformer[] = [
16+
(template, data) =>
17+
template
18+
.replace('%sapper.base%', () => `<base href="${data.req.baseUrl}/">`)
19+
.replace(
20+
'%sapper.scripts%',
21+
() => `<script${data.nonce_attr}>${data.script}</script>`
22+
)
23+
.replace('%sapper.html%', () => data.html)
24+
.replace('%sapper.head%', () => data.head)
25+
.replace('%sapper.styles%', () => data.styles)
26+
.replace(/%sapper\.cspnonce%/g, () => data.nonce_value)
27+
];
28+
29+
export function registerTemplateTransformer(transformer: Transformer) {
30+
transformers.splice(-1, 0, transformer);
31+
}
32+
33+
export function transformTemplate(template: string, data: TransformData) {
34+
return transformers.reduce(
35+
(acc, transformer) => transformer(acc, data),
36+
template
37+
);
38+
}

site/content/docs/01-structure.md

+57
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ This file is a template for responses from the server. Sapper will inject conten
9494
* `%sapper.scripts%` — script tags for the client-side app
9595
* `%sapper.cspnonce%` — CSP nonce taken from `res.locals.nonce` (see [Content Security Policy (CSP)](docs#Content_Security_Policy_CSP))
9696

97+
To learn how to change these replacements or replace your own tags in the template file, see [Template Transforms](docs#Template_Transforms)
9798

9899
### src/routes
99100

@@ -111,3 +112,59 @@ Sapper doesn't serve these files — you'd typically use [sirv](https://github.c
111112
### rollup.config.js / webpack.config.js
112113

113114
Sapper can use [Rollup](https://rollupjs.org/) or [webpack](https://webpack.js.org/) to bundle your app. You probably won't need to change the config, but if you want to (for example to add new loaders or plugins), you can.
115+
116+
### Template Transforms
117+
118+
Sapper provides a few tags that it will automatically replace inside the `src/.template.html` file. These replacements can be changed by providing your own template transformer.
119+
120+
You can register a template transformer inside the `src/server.js` file.
121+
```diff
122+
import polka from 'polka';
123+
import * as sapper from '@sapper/server';
124+
+ import { makeHash } from './my-helpers';
125+
126+
import { start } from '../../common.js';
127+
128+
+ sapper.registerTemplateTransformer((template) =>
129+
+ template.replace(
130+
+ '%mytag.globalCss%',
131+
+ () => 'global.css?v=' + makeHash('global.css')
132+
+ )
133+
+ )
134+
+
135+
const app = polka()
136+
.use(sapper.middleware());
137+
138+
start(app);
139+
```
140+
141+
The function that you pass to `registerTemplateTransformer` will be given two arguments:
142+
143+
1. `template`: string contents of the template before Sapper's replacements
144+
2. `data`: an object containing all the values you'd need to completely replace the tags that Sapper itself replaces:
145+
- `html`: string
146+
- `head`: string
147+
- `styles`: string
148+
- `script`: string
149+
- `nonce_value`: string
150+
- `nonce_attr`: string
151+
- `req`: the request object
152+
153+
You must return a string which is the full contents of the template file as you'd like them to appear. Any of Sapper's own tags that you do not replace will be replaced as normal by Sapper.
154+
155+
```ts
156+
type Transformer = (
157+
template: string,
158+
data: {
159+
html: string
160+
head: string
161+
styles: string
162+
script: string
163+
nonce_value: string
164+
nonce_attr: string
165+
req: SapperRequest
166+
}
167+
) => string
168+
```
169+
170+
You may call the function multiple times. Transformers will run in the order in which they are registered, with the default Sapper transformer running last.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import resolve from '@rollup/plugin-node-resolve';
2+
import replace from '@rollup/plugin-replace';
3+
import svelte from 'rollup-plugin-svelte';
4+
5+
const mode = process.env.NODE_ENV;
6+
const dev = mode === 'development';
7+
8+
const config = require('../../../config/rollup.js');
9+
10+
export default {
11+
client: {
12+
input: config.client.input(),
13+
output: config.client.output(),
14+
plugins: [
15+
replace({
16+
'process.browser': true,
17+
'process.env.NODE_ENV': JSON.stringify(mode)
18+
}),
19+
svelte({
20+
dev,
21+
hydratable: true,
22+
emitCss: true
23+
}),
24+
resolve()
25+
]
26+
},
27+
28+
server: {
29+
input: config.server.input(),
30+
output: config.server.output(),
31+
plugins: [
32+
replace({
33+
'process.browser': false,
34+
'process.env.NODE_ENV': JSON.stringify(mode)
35+
}),
36+
svelte({
37+
generate: 'ssr',
38+
dev
39+
}),
40+
resolve({
41+
preferBuiltins: true
42+
})
43+
],
44+
external: ['sirv', 'polka']
45+
},
46+
47+
serviceworker: {
48+
input: config.serviceworker.input(),
49+
output: config.serviceworker.output(),
50+
plugins: [
51+
resolve(),
52+
replace({
53+
'process.browser': true,
54+
'process.env.NODE_ENV': JSON.stringify(mode)
55+
})
56+
]
57+
}
58+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as sapper from '@sapper/app';
2+
3+
window.start = () => sapper.start({
4+
target: document.querySelector('#sapper')
5+
});
6+
7+
window.prefetchRoutes = () => sapper.prefetchRoutes();
8+
window.prefetch = href => sapper.prefetch(href);
9+
window.goto = href => sapper.goto(href);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import fs from 'fs';
2+
import crypto from 'crypto';
3+
4+
export function staticFileHash(pathToFile) {
5+
return (
6+
pathToFile +
7+
'?v=' +
8+
crypto
9+
.createHash('md5')
10+
.update(fs.readFileSync('static/' + pathToFile, 'utf-8'))
11+
.digest('hex')
12+
);
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
export let status;
3+
export let error;
4+
</script>
5+
6+
<h1>{status}</h1>
7+
8+
<p>{error.message}</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Great success!</h1>
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import polka from 'polka';
2+
import * as sapper from '@sapper/server';
3+
import { staticFileHash } from './file-hash';
4+
5+
import { start } from '../../common.js';
6+
7+
const dev = process.env.NODE_ENV === 'development';
8+
9+
sapper.registerTemplateTransformer((template) =>
10+
template.replace('%arbitrary.globalCss%', () =>
11+
dev ? 'global.css' : staticFileHash('global.css')
12+
)
13+
);
14+
15+
const app = polka()
16+
.use(sapper.middleware());
17+
18+
start(app);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as sapper from '@sapper/service-worker';
2+
3+
const ASSETS = `cache${sapper.timestamp}`;
4+
5+
// `shell` is an array of all the files generated by webpack,
6+
// `files` is an array of everything in the `static` directory
7+
const to_cache = sapper.shell.concat(sapper.files);
8+
const cached = new Set(to_cache);
9+
10+
self.addEventListener('install', event => {
11+
event.waitUntil(
12+
caches
13+
.open(ASSETS)
14+
.then(cache => cache.addAll(to_cache))
15+
.then(() => {
16+
self.skipWaiting();
17+
})
18+
);
19+
});
20+
21+
self.addEventListener('activate', event => {
22+
event.waitUntil(
23+
caches.keys().then(async keys => {
24+
// delete old caches
25+
for (const key of keys) {
26+
if (key !== ASSETS) await caches.delete(key);
27+
}
28+
29+
self.clients.claim();
30+
})
31+
);
32+
});
33+
34+
self.addEventListener('fetch', event => {
35+
if (event.request.method !== 'GET') return;
36+
37+
const url = new URL(event.request.url);
38+
39+
// don't try to handle e.g. data: URIs
40+
if (!url.protocol.startsWith('http')) return;
41+
42+
// ignore dev server requests
43+
if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
44+
45+
// always serve assets and webpack-generated files from cache
46+
if (url.host === self.location.host && cached.has(url.pathname)) {
47+
event.respondWith(caches.match(event.request));
48+
return;
49+
}
50+
51+
// for pages, you might want to serve a shell `index.html` file,
52+
// which Sapper has generated for you. It's not right for every
53+
// app, but if it's right for yours then uncomment this section
54+
/*
55+
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
56+
event.respondWith(caches.match('/index.html'));
57+
return;
58+
}
59+
*/
60+
61+
if (event.request.cache === 'only-if-cached') return;
62+
63+
// for everything else, try the network first, falling back to
64+
// cache if the user is offline. (If the pages never change, you
65+
// might prefer a cache-first approach to a network-first one.)
66+
event.respondWith(
67+
caches
68+
.open(`offline${sapper.timestamp}`)
69+
.then(async cache => {
70+
try {
71+
const response = await fetch(event.request);
72+
cache.put(event.request, response.clone());
73+
return response;
74+
} catch (err) {
75+
const response = await cache.match(event.request);
76+
if (response) return response;
77+
78+
throw err;
79+
}
80+
})
81+
);
82+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset='utf-8'>
5+
%sapper.base%
6+
7+
<link id="globalCssHashed" rel="stylesheet" href="%arbitrary.globalCss%" />
8+
9+
%sapper.styles%
10+
%sapper.head%
11+
</head>
12+
<body>
13+
<div id='sapper'>%sapper.html%</div>
14+
%sapper.scripts%
15+
</body>
16+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h1 {
2+
color: purple;
3+
}

0 commit comments

Comments
 (0)