Skip to content

Commit e100b42

Browse files
authored
make static assets immutable (#3222)
* make static assets immutable * write custom _headers file * remove errant semis * add custom headers to vercel routes config * immutable assets in adapter-cloudflare-workers * changesets * tidy up
1 parent 1a6adc8 commit e100b42

File tree

13 files changed

+170
-58
lines changed

13 files changed

+170
-58
lines changed

.changeset/brave-weeks-allow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
Expose appDir to adapters

.changeset/witty-meals-tie.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@sveltejs/adapter-cloudflare': patch
3+
'@sveltejs/adapter-cloudflare-workers': patch
4+
'@sveltejs/adapter-netlify': patch
5+
'@sveltejs/adapter-vercel': patch
6+
---
7+
8+
Add immutable cache headers to generated assets

packages/adapter-cloudflare-workers/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Now you should get some details from Cloudflare. You should get your:
4747
1. Account ID
4848
2. And your Zone-ID (Optional)
4949

50-
Get them by visiting your Cloudflare-Dashboard and click on any domain. There, you can scroll down and on the left, you can see your details under **API**.
50+
Get them by visiting your [Cloudflare dashboard](https://dash.cloudflare.com) and click on any domain. There, you can scroll down and on the left, you can see your details under **API**.
5151

5252
Then configure your sites build directory and your account-details in the config file:
5353

packages/adapter-cloudflare-workers/files/entry.js

+43-24
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,54 @@
11
import { App } from 'APP';
2-
import { manifest } from './manifest.js';
3-
import { getAssetFromKV, NotFoundError } from '@cloudflare/kv-asset-handler';
2+
import { manifest, prerendered } from './manifest.js';
3+
import { getAssetFromKV } from '@cloudflare/kv-asset-handler';
44

55
const app = new App(manifest);
66

7-
addEventListener('fetch', (event) => {
7+
const prefix = `/${manifest.appDir}/`;
8+
9+
addEventListener('fetch', (/** @type {FetchEvent} */ event) => {
810
event.respondWith(handle(event));
911
});
1012

13+
/**
14+
* @param {FetchEvent} event
15+
* @returns {Promise<Response>}
16+
*/
1117
async function handle(event) {
12-
// try static files first
13-
if (event.request.method == 'GET') {
14-
try {
15-
// TODO rather than attempting to get an asset,
16-
// use the asset manifest to see if it exists
17-
return await getAssetFromKV(event);
18-
} catch (e) {
19-
if (!(e instanceof NotFoundError)) {
20-
return new Response('Error loading static asset:' + (e.message || e.toString()), {
21-
status: 500
22-
});
18+
const { request } = event;
19+
20+
const url = new URL(request.url);
21+
22+
// generated assets
23+
if (url.pathname.startsWith(prefix)) {
24+
const res = await getAssetFromKV(event);
25+
return new Response(res.body, {
26+
headers: {
27+
'cache-control': 'public, immutable, max-age=31536000',
28+
'content-type': res.headers.get('content-type')
2329
}
24-
}
30+
});
2531
}
2632

27-
// fall back to an app route
28-
const request = event.request;
33+
// prerendered pages and index.html files
34+
const pathname = url.pathname.replace(/\/$/, '');
35+
let file = pathname.substring(1);
2936

37+
try {
38+
file = decodeURIComponent(file);
39+
} catch (err) {
40+
// ignore
41+
}
42+
43+
if (
44+
manifest.assets.has(file) ||
45+
manifest.assets.has(file + '/index.html') ||
46+
prerendered.has(pathname || '/')
47+
) {
48+
return await getAssetFromKV(event);
49+
}
50+
51+
// dynamically-generated pages
3052
try {
3153
const rendered = await app.render({
3254
url: request.url,
@@ -38,14 +60,14 @@ async function handle(event) {
3860
if (rendered) {
3961
return new Response(rendered.body, {
4062
status: rendered.status,
41-
headers: makeHeaders(rendered.headers)
63+
headers: make_headers(rendered.headers)
4264
});
4365
}
4466
} catch (e) {
4567
return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 });
4668
}
4769

48-
return new Response({
70+
return new Response('Not Found', {
4971
status: 404,
5072
statusText: 'Not Found'
5173
});
@@ -56,11 +78,8 @@ async function read(request) {
5678
return new Uint8Array(await request.arrayBuffer());
5779
}
5880

59-
/**
60-
* @param {Record<string, string | string[]>} headers
61-
* @returns {Request}
62-
*/
63-
function makeHeaders(headers) {
81+
/** @param {Record<string, string | string[]>} headers */
82+
function make_headers(headers) {
6483
const result = new Headers();
6584
for (const header in headers) {
6685
const value = headers[header];

packages/adapter-cloudflare-workers/index.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function () {
2323
builder.rimraf(entrypoint);
2424

2525
builder.log.info('Prerendering static pages...');
26-
await builder.prerender({
26+
const { paths } = await builder.prerender({
2727
dest: bucket
2828
});
2929

@@ -47,7 +47,7 @@ export default function () {
4747
`${tmp}/manifest.js`,
4848
`export const manifest = ${builder.generateManifest({
4949
relativePath
50-
})};\n`
50+
})};\n\nexport const prerendered = new Set(${JSON.stringify(paths)});\n`
5151
);
5252

5353
await esbuild.build({
@@ -67,6 +67,7 @@ export default function () {
6767
};
6868
}
6969

70+
/** @param {import('@sveltejs/kit').Builder} builder */
7071
function validate_config(builder) {
7172
if (existsSync('wrangler.toml')) {
7273
let wrangler_config;

packages/adapter-cloudflare-workers/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"esbuild": "^0.13.15"
3232
},
3333
"devDependencies": {
34+
"@cloudflare/kv-asset-handler": "^0.2.0",
35+
"@cloudflare/workers-types": "^3.3.0",
3436
"@sveltejs/kit": "workspace:*"
3537
}
3638
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"allowJs": true,
4+
"checkJs": true,
5+
"noEmit": true,
6+
"noImplicitAny": true,
7+
"target": "es2020",
8+
"module": "es2020",
9+
"moduleResolution": "node",
10+
"allowSyntheticDefaultImports": true,
11+
"types": ["@cloudflare/workers-types"]
12+
},
13+
"include": ["./index.js", "files"]
14+
}

packages/adapter-cloudflare/files/worker.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,20 @@ export default {
1010
const url = new URL(req.url);
1111

1212
// static assets
13-
if (url.pathname.startsWith(prefix)) return env.ASSETS.fetch(req);
13+
if (url.pathname.startsWith(prefix)) {
14+
/** @type {Response} */
15+
const res = await env.ASSETS.fetch(req);
16+
17+
return new Response(res.body, {
18+
headers: {
19+
// include original cache headers, minus cache-control which
20+
// is overridden, and etag which is no longer useful
21+
'cache-control': 'public, immutable, max-age=31536000',
22+
'content-type': res.headers.get('content-type'),
23+
'x-robots-tag': 'noindex'
24+
}
25+
});
26+
}
1427

1528
// prerendered pages and index.html files
1629
const pathname = url.pathname.replace(/\/$/, '');

packages/adapter-netlify/index.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,13 @@ export default function ({ split = false } = {}) {
129129
builder.copy('_redirects', redirect_file);
130130
appendFileSync(redirect_file, `\n\n${redirects.join('\n')}`);
131131

132-
// TODO write a _headers file that makes client-side assets immutable
132+
builder.log.minor('Writing custom headers...');
133+
const headers_file = join(publish, '_headers');
134+
builder.copy('_headers', headers_file);
135+
appendFileSync(
136+
headers_file,
137+
`\n\n/${builder.appDir}/*\n cache-control: public\n cache-control: immutable\n cache-control: max-age=31536000\n`
138+
);
133139
}
134140
};
135141
}

packages/adapter-vercel/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export default function () {
6868
writeFileSync(
6969
`${dir}/config/routes.json`,
7070
JSON.stringify([
71+
{
72+
src: `/${builder.appDir}/.+`,
73+
headers: {
74+
'cache-control': 'public, immutable, max-age=31536000'
75+
}
76+
},
7177
{
7278
handle: 'filesystem'
7379
},

packages/kit/src/core/adapt/builder.js

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export function create_builder({ cwd, config, build_data, log }) {
3232
mkdirp,
3333
copy,
3434

35+
appDir: config.kit.appDir,
36+
3537
createEntries(fn) {
3638
generated_manifest = true;
3739

packages/kit/types/config.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export interface Builder {
3838
rimraf(dir: string): void;
3939
mkdirp(dir: string): void;
4040

41+
appDir: string;
42+
4143
/**
4244
* Create entry points that map to individual functions
4345
* @param fn A function that groups a set of routes into an entry point

0 commit comments

Comments
 (0)