Skip to content

Commit

Permalink
make static assets immutable (#3222)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Rich-Harris authored Jan 7, 2022
1 parent 1a6adc8 commit e100b42
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 58 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-weeks-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Expose appDir to adapters
8 changes: 8 additions & 0 deletions .changeset/witty-meals-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sveltejs/adapter-cloudflare': patch
'@sveltejs/adapter-cloudflare-workers': patch
'@sveltejs/adapter-netlify': patch
'@sveltejs/adapter-vercel': patch
---

Add immutable cache headers to generated assets
2 changes: 1 addition & 1 deletion packages/adapter-cloudflare-workers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Now you should get some details from Cloudflare. You should get your:
1. Account ID
2. And your Zone-ID (Optional)

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

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

Expand Down
67 changes: 43 additions & 24 deletions packages/adapter-cloudflare-workers/files/entry.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,54 @@
import { App } from 'APP';
import { manifest } from './manifest.js';
import { getAssetFromKV, NotFoundError } from '@cloudflare/kv-asset-handler';
import { manifest, prerendered } from './manifest.js';
import { getAssetFromKV } from '@cloudflare/kv-asset-handler';

const app = new App(manifest);

addEventListener('fetch', (event) => {
const prefix = `/${manifest.appDir}/`;

addEventListener('fetch', (/** @type {FetchEvent} */ event) => {
event.respondWith(handle(event));
});

/**
* @param {FetchEvent} event
* @returns {Promise<Response>}
*/
async function handle(event) {
// try static files first
if (event.request.method == 'GET') {
try {
// TODO rather than attempting to get an asset,
// use the asset manifest to see if it exists
return await getAssetFromKV(event);
} catch (e) {
if (!(e instanceof NotFoundError)) {
return new Response('Error loading static asset:' + (e.message || e.toString()), {
status: 500
});
const { request } = event;

const url = new URL(request.url);

// generated assets
if (url.pathname.startsWith(prefix)) {
const res = await getAssetFromKV(event);
return new Response(res.body, {
headers: {
'cache-control': 'public, immutable, max-age=31536000',
'content-type': res.headers.get('content-type')
}
}
});
}

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

try {
file = decodeURIComponent(file);
} catch (err) {
// ignore
}

if (
manifest.assets.has(file) ||
manifest.assets.has(file + '/index.html') ||
prerendered.has(pathname || '/')
) {
return await getAssetFromKV(event);
}

// dynamically-generated pages
try {
const rendered = await app.render({
url: request.url,
Expand All @@ -38,14 +60,14 @@ async function handle(event) {
if (rendered) {
return new Response(rendered.body, {
status: rendered.status,
headers: makeHeaders(rendered.headers)
headers: make_headers(rendered.headers)
});
}
} catch (e) {
return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 });
}

return new Response({
return new Response('Not Found', {
status: 404,
statusText: 'Not Found'
});
Expand All @@ -56,11 +78,8 @@ async function read(request) {
return new Uint8Array(await request.arrayBuffer());
}

/**
* @param {Record<string, string | string[]>} headers
* @returns {Request}
*/
function makeHeaders(headers) {
/** @param {Record<string, string | string[]>} headers */
function make_headers(headers) {
const result = new Headers();
for (const header in headers) {
const value = headers[header];
Expand Down
5 changes: 3 additions & 2 deletions packages/adapter-cloudflare-workers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function () {
builder.rimraf(entrypoint);

builder.log.info('Prerendering static pages...');
await builder.prerender({
const { paths } = await builder.prerender({
dest: bucket
});

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

await esbuild.build({
Expand All @@ -67,6 +67,7 @@ export default function () {
};
}

/** @param {import('@sveltejs/kit').Builder} builder */
function validate_config(builder) {
if (existsSync('wrangler.toml')) {
let wrangler_config;
Expand Down
2 changes: 2 additions & 0 deletions packages/adapter-cloudflare-workers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"esbuild": "^0.13.15"
},
"devDependencies": {
"@cloudflare/kv-asset-handler": "^0.2.0",
"@cloudflare/workers-types": "^3.3.0",
"@sveltejs/kit": "workspace:*"
}
}
14 changes: 14 additions & 0 deletions packages/adapter-cloudflare-workers/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"noImplicitAny": true,
"target": "es2020",
"module": "es2020",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"types": ["@cloudflare/workers-types"]
},
"include": ["./index.js", "files"]
}
15 changes: 14 additions & 1 deletion packages/adapter-cloudflare/files/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,20 @@ export default {
const url = new URL(req.url);

// static assets
if (url.pathname.startsWith(prefix)) return env.ASSETS.fetch(req);
if (url.pathname.startsWith(prefix)) {
/** @type {Response} */
const res = await env.ASSETS.fetch(req);

return new Response(res.body, {
headers: {
// include original cache headers, minus cache-control which
// is overridden, and etag which is no longer useful
'cache-control': 'public, immutable, max-age=31536000',
'content-type': res.headers.get('content-type'),
'x-robots-tag': 'noindex'
}
});
}

// prerendered pages and index.html files
const pathname = url.pathname.replace(/\/$/, '');
Expand Down
8 changes: 7 additions & 1 deletion packages/adapter-netlify/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ export default function ({ split = false } = {}) {
builder.copy('_redirects', redirect_file);
appendFileSync(redirect_file, `\n\n${redirects.join('\n')}`);

// TODO write a _headers file that makes client-side assets immutable
builder.log.minor('Writing custom headers...');
const headers_file = join(publish, '_headers');
builder.copy('_headers', headers_file);
appendFileSync(
headers_file,
`\n\n/${builder.appDir}/*\n cache-control: public\n cache-control: immutable\n cache-control: max-age=31536000\n`
);
}
};
}
Expand Down
6 changes: 6 additions & 0 deletions packages/adapter-vercel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export default function () {
writeFileSync(
`${dir}/config/routes.json`,
JSON.stringify([
{
src: `/${builder.appDir}/.+`,
headers: {
'cache-control': 'public, immutable, max-age=31536000'
}
},
{
handle: 'filesystem'
},
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export function create_builder({ cwd, config, build_data, log }) {
mkdirp,
copy,

appDir: config.kit.appDir,

createEntries(fn) {
generated_manifest = true;

Expand Down
2 changes: 2 additions & 0 deletions packages/kit/types/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export interface Builder {
rimraf(dir: string): void;
mkdirp(dir: string): void;

appDir: string;

/**
* Create entry points that map to individual functions
* @param fn A function that groups a set of routes into an entry point
Expand Down
Loading

0 comments on commit e100b42

Please sign in to comment.