Created
May 26, 2020 05:55
-
-
Save jdanyow/afce63c15d6d8e68e1356fd93a19ad03 to your computer and use it in GitHub Desktop.
Cloudflare workers static asset middleware using @cfworker/web
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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