Skip to content

Instantly share code, notes, and snippets.

@jdanyow
Created May 26, 2020 05:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdanyow/afce63c15d6d8e68e1356fd93a19ad03 to your computer and use it in GitHub Desktop.
Save jdanyow/afce63c15d6d8e68e1356fd93a19ad03 to your computer and use it in GitHub Desktop.
Cloudflare workers static asset middleware using @cfworker/web
import { Middleware } from '@cfworker/web';
import { KVNamespace } from '@cloudflare/workers-types';
declare const __STATIC_CONTENT_MANIFEST: string | undefined;
declare const __STATIC_CONTENT: KVNamespace | undefined;
export class MissingStaticAssetError extends Error {
constructor(
public readonly pathname: string,
public readonly key: string,
public readonly manifest: Record<string, string>
) {
super(`Missing static asset for pathname "${pathname}" => "${key}"`);
this.name = this.constructor.name;
}
}
const defaultManifest: Record<string, string> =
typeof __STATIC_CONTENT_MANIFEST === 'undefined'
? {}
: JSON.parse(__STATIC_CONTENT_MANIFEST);
const defaultKV =
typeof __STATIC_CONTENT === 'undefined'
? (({ get: () => Promise.resolve(null) } as any) as KVNamespace)
: __STATIC_CONTENT;
export const contentTypes: Record<string, string> = {
css: 'text/css',
html: 'text/html',
js: 'application/javascript',
json: 'application/json',
svg: 'application/svg+xml',
txt: 'text/plain'
};
export const defaultContentType = contentTypes['txt'];
export function getExtension(filename: string) {
return (/\.(\w{1,4})$/.exec(filename) || ['txt'])[1];
}
export function getContentType(filename: string): string {
const extension = getExtension(filename);
return contentTypes[extension] || defaultContentType;
}
function middlewareFactory(
manifest = defaultManifest,
kv = defaultKV,
cache = caches.default
): Middleware {
return async (context, next) => {
const {
req: { method, url },
res
} = context;
if (method !== 'GET' && method !== 'HEAD') {
await next();
return;
}
const pathname = url.pathname === '/' ? '/index.html' : url.pathname;
const filename = pathname.substr(1);
const key = manifest[filename];
if (!key) {
await next();
return;
}
const type = getContentType(filename);
const isHashed = /\.\w+\.\w+$/.test(pathname);
const browserTTL = isHashed
? `public, max-age=${60 * 60 * 24 * 7}, immutable` // 2d
: `public, max-age=${60 * 15}`; // 15m
const edgeTTL = browserTTL;
if (method === 'HEAD') {
res.status = 201;
res.type = type;
res.headers.set('cache-control', browserTTL);
await next();
return;
}
const cacheKey = `${url.origin}/${key}`;
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) {
res.type = type;
res.headers.set('cf-cache-status', 'hit');
res.headers.set('cache-control', browserTTL);
res.body = cachedResponse.body;
await next();
return;
}
const body = await kv.get(key, 'stream');
if (body === null) {
throw new MissingStaticAssetError(pathname, key, manifest);
}
const [resBody, cacheBody] = body.tee();
res.type = type;
res.headers.set('cf-cache-status', 'miss');
res.headers.set('cache-control', browserTTL);
res.body = resBody;
context.waitUntil(
cache.put(
cacheKey,
new Response(cacheBody, {
headers: {
'content-type': type,
'cache-control': edgeTTL
}
})
)
);
await next();
};
}
export const staticContent = middlewareFactory;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment