diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a177ab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +# https://editorconfig.org +root = true + +[*] +end_of_line = lf +indent_style = tab +tab_width = 2 diff --git a/.prettierrc b/.prettierrc index d7db2d6..fe94b7b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,8 @@ { - "endOfLine": "lf", - "trailingComma": "all", - "singleQuote": true, - "semi": false, - "printWidth": 100 -} + "endOfLine": "lf", + "trailingComma": "all", + "singleQuote": true, + "useTabs": true, + "semi": false, + "printWidth": 100 +} \ No newline at end of file diff --git a/README.md b/README.md index c36becc..8b1e588 100644 --- a/README.md +++ b/README.md @@ -76,32 +76,32 @@ import manifestJSON from '__STATIC_CONTENT_MANIFEST' const assetManifest = JSON.parse(manifestJSON) export default { - async fetch(request, env, ctx) { - if (request.url.includes('/docs')) { - try { - return await getAssetFromKV( - { - request, - waitUntil(promise) { - return ctx.waitUntil(promise) - }, - }, - { - ASSET_NAMESPACE: env.__STATIC_CONTENT, - ASSET_MANIFEST: assetManifest, - }, - ) - } catch (e) { - if (e instanceof NotFoundError) { - // ... - } else if (e instanceof MethodNotAllowedError) { - // ... - } else { - return new Response('An unexpected error occurred', { status: 500 }) - } - } - } else return fetch(request) - }, + async fetch(request, env, ctx) { + if (request.url.includes('/docs')) { + try { + return await getAssetFromKV( + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise) + }, + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + ASSET_MANIFEST: assetManifest, + }, + ) + } catch (e) { + if (e instanceof NotFoundError) { + // ... + } else if (e instanceof MethodNotAllowedError) { + // ... + } else { + return new Response('An unexpected error occurred', { status: 500 }) + } + } + } else return fetch(request) + }, } ``` @@ -111,23 +111,23 @@ export default { import { getAssetFromKV, NotFoundError, MethodNotAllowedError } from '@cloudflare/kv-asset-handler' addEventListener('fetch', (event) => { - event.respondWith(handleEvent(event)) + event.respondWith(handleEvent(event)) }) async function handleEvent(event) { - if (event.request.url.includes('/docs')) { - try { - return await getAssetFromKV(event) - } catch (e) { - if (e instanceof NotFoundError) { - // ... - } else if (e instanceof MethodNotAllowedError) { - // ... - } else { - return new Response('An unexpected error occurred', { status: 500 }) - } - } - } else return fetch(event.request) + if (event.request.url.includes('/docs')) { + try { + return await getAssetFromKV(event) + } catch (e) { + if (e instanceof NotFoundError) { + // ... + } else if (e instanceof MethodNotAllowedError) { + // ... + } else { + return new Response('An unexpected error occurred', { status: 500 }) + } + } + } else return fetch(event.request) } ``` @@ -173,9 +173,9 @@ type: object ```javascript let cacheControl = { - browserTTL: null, // do not set cache control ttl on responses - edgeTTL: 2 * 60 * 60 * 24, // 2 days - bypassCache: false, // do not bypass Cloudflare's cache + browserTTL: null, // do not set cache control ttl on responses + edgeTTL: 2 * 60 * 60 * 24, // 2 days + bypassCache: false, // do not bypass Cloudflare's cache } ``` @@ -213,15 +213,15 @@ In ES Modules format, this argument is required, and can be gotten from `env`. ```js return getAssetFromKV( - { - request, - waitUntil(promise) { - return ctx.waitUntil(promise) - }, - }, - { - ASSET_NAMESPACE: env.__STATIC_CONTENT, - }, + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise) + }, + }, + { + ASSET_NAMESPACE: env.__STATIC_CONTENT, + }, ) ``` @@ -249,16 +249,16 @@ let manifest = JSON.parse(manifestJSON) manifest['index.html'] = 'index.special.html' return getAssetFromKV( - { - request, - waitUntil(promise) { - return ctx.waitUntil(promise) - }, - }, - { - ASSET_MANIFEST: manifest, - // ... - }, + { + request, + waitUntil(promise) { + return ctx.waitUntil(promise) + }, + }, + { + ASSET_MANIFEST: manifest, + // ... + }, ) ``` @@ -328,7 +328,7 @@ To turn `etags` **off**, you must bypass cache: ```js /* Turn etags off */ let cacheControl = { - bypassCache: true, + bypassCache: true, } ``` diff --git a/src/index.ts b/src/index.ts index 17066dd..4a745a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,43 @@ import * as mime from 'mime' import { - Options, - CacheControl, - MethodNotAllowedError, - NotFoundError, - InternalError, - AssetManifestType, + Options, + CacheControl, + MethodNotAllowedError, + NotFoundError, + InternalError, + AssetManifestType, } from './types' declare global { - var __STATIC_CONTENT: any, __STATIC_CONTENT_MANIFEST: string + var __STATIC_CONTENT: any, __STATIC_CONTENT_MANIFEST: string } const defaultCacheControl: CacheControl = { - browserTTL: null, - edgeTTL: 2 * 60 * 60 * 24, // 2 days - bypassCache: false, // do not bypass Cloudflare's cache + browserTTL: null, + edgeTTL: 2 * 60 * 60 * 24, // 2 days + bypassCache: false, // do not bypass Cloudflare's cache } const parseStringAsObject = (maybeString: string | T): T => - typeof maybeString === 'string' ? (JSON.parse(maybeString) as T) : maybeString + typeof maybeString === 'string' ? (JSON.parse(maybeString) as T) : maybeString const getAssetFromKVDefaultOptions: Partial = { - ASSET_NAMESPACE: typeof __STATIC_CONTENT !== 'undefined' ? __STATIC_CONTENT : undefined, - ASSET_MANIFEST: - typeof __STATIC_CONTENT_MANIFEST !== 'undefined' - ? parseStringAsObject(__STATIC_CONTENT_MANIFEST) - : {}, - cacheControl: defaultCacheControl, - defaultMimeType: 'text/plain', - defaultDocument: 'index.html', - pathIsEncoded: false, - defaultETag: 'strong', + ASSET_NAMESPACE: typeof __STATIC_CONTENT !== 'undefined' ? __STATIC_CONTENT : undefined, + ASSET_MANIFEST: + typeof __STATIC_CONTENT_MANIFEST !== 'undefined' + ? parseStringAsObject(__STATIC_CONTENT_MANIFEST) + : {}, + cacheControl: defaultCacheControl, + defaultMimeType: 'text/plain', + defaultDocument: 'index.html', + pathIsEncoded: false, + defaultETag: 'strong', } function assignOptions(options?: Partial): Options { - // Assign any missing options passed in to the default - // options.mapRequestToAsset is handled manually later - return Object.assign({}, getAssetFromKVDefaultOptions, options) + // Assign any missing options passed in to the default + // options.mapRequestToAsset is handled manually later + return Object.assign({}, getAssetFromKVDefaultOptions, options) } /** @@ -48,23 +48,23 @@ function assignOptions(options?: Partial): Options { * @param {Request} request incoming request */ const mapRequestToAsset = (request: Request, options?: Partial) => { - options = assignOptions(options) - - const parsedUrl = new URL(request.url) - let pathname = parsedUrl.pathname - - if (pathname.endsWith('/')) { - // If path looks like a directory append options.defaultDocument - // e.g. If path is /about/ -> /about/index.html - pathname = pathname.concat(options.defaultDocument) - } else if (!mime.getType(pathname)) { - // If path doesn't look like valid content - // e.g. /about.me -> /about.me/index.html - pathname = pathname.concat('/' + options.defaultDocument) - } - - parsedUrl.pathname = pathname - return new Request(parsedUrl.toString(), request) + options = assignOptions(options) + + const parsedUrl = new URL(request.url) + let pathname = parsedUrl.pathname + + if (pathname.endsWith('/')) { + // If path looks like a directory append options.defaultDocument + // e.g. If path is /about/ -> /about/index.html + pathname = pathname.concat(options.defaultDocument) + } else if (!mime.getType(pathname)) { + // If path doesn't look like valid content + // e.g. /about.me -> /about.me/index.html + pathname = pathname.concat('/' + options.defaultDocument) + } + + parsedUrl.pathname = pathname + return new Request(parsedUrl.toString(), request) } /** @@ -73,24 +73,24 @@ const mapRequestToAsset = (request: Request, options?: Partial) => { * @param {Request} request incoming request */ function serveSinglePageApp(request: Request, options?: Partial): Request { - options = assignOptions(options) - - // First apply the default handler, which already has logic to detect - // paths that should map to HTML files. - request = mapRequestToAsset(request, options) - - const parsedUrl = new URL(request.url) - - // Detect if the default handler decided to map to - // a HTML file in some specific directory. - if (parsedUrl.pathname.endsWith('.html')) { - // If expected HTML file was missing, just return the root index.html (or options.defaultDocument) - return new Request(`${parsedUrl.origin}/${options.defaultDocument}`, request) - } else { - // The default handler decided this is not an HTML page. It's probably - // an image, CSS, or JS file. Leave it as-is. - return request - } + options = assignOptions(options) + + // First apply the default handler, which already has logic to detect + // paths that should map to HTML files. + request = mapRequestToAsset(request, options) + + const parsedUrl = new URL(request.url) + + // Detect if the default handler decided to map to + // a HTML file in some specific directory. + if (parsedUrl.pathname.endsWith('.html')) { + // If expected HTML file was missing, just return the root index.html (or options.defaultDocument) + return new Request(`${parsedUrl.origin}/${options.defaultDocument}`, request) + } else { + // The default handler decided this is not an HTML page. It's probably + // an image, CSS, or JS file. Leave it as-is. + return request + } } /** @@ -106,210 +106,210 @@ function serveSinglePageApp(request: Request, options?: Partial): Reque * */ type Evt = { - request: Request - waitUntil: (promise: Promise) => void + request: Request + waitUntil: (promise: Promise) => void } const getAssetFromKV = async (event: Evt, options?: Partial): Promise => { - options = assignOptions(options) - - const request = event.request - const ASSET_NAMESPACE = options.ASSET_NAMESPACE - const ASSET_MANIFEST = parseStringAsObject(options.ASSET_MANIFEST) - - if (typeof ASSET_NAMESPACE === 'undefined') { - throw new InternalError(`there is no KV namespace bound to the script`) - } - - const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, '') // strip any preceding /'s - let pathIsEncoded = options.pathIsEncoded - let requestKey - // if options.mapRequestToAsset is explicitly passed in, always use it and assume user has own intentions - // otherwise handle request as normal, with default mapRequestToAsset below - if (options.mapRequestToAsset) { - requestKey = options.mapRequestToAsset(request) - } else if (ASSET_MANIFEST[rawPathKey]) { - requestKey = request - } else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) { - pathIsEncoded = true - requestKey = request - } else { - const mappedRequest = mapRequestToAsset(request) - const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, '') - if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) { - pathIsEncoded = true - requestKey = mappedRequest - } else { - // use default mapRequestToAsset - requestKey = mapRequestToAsset(request, options) - } - } - - const SUPPORTED_METHODS = ['GET', 'HEAD'] - if (!SUPPORTED_METHODS.includes(requestKey.method)) { - throw new MethodNotAllowedError(`${requestKey.method} is not a valid request method`) - } - - const parsedUrl = new URL(requestKey.url) - const pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary - - // pathKey is the file path to look up in the manifest - let pathKey = pathname.replace(/^\/+/, '') // remove prepended / - - // @ts-ignore - const cache = caches.default - let mimeType = mime.getType(pathKey) || options.defaultMimeType - if (mimeType.startsWith('text') || mimeType === 'application/javascript') { - mimeType += '; charset=utf-8' - } - - let shouldEdgeCache = false // false if storing in KV by raw file path i.e. no hash - // check manifest for map from file path to hash - if (typeof ASSET_MANIFEST !== 'undefined') { - if (ASSET_MANIFEST[pathKey]) { - pathKey = ASSET_MANIFEST[pathKey] - // if path key is in asset manifest, we can assume it contains a content hash and can be cached - shouldEdgeCache = true - } - } - - // TODO this excludes search params from cache, investigate ideal behavior - let cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request) - - // if argument passed in for cacheControl is a function then - // evaluate that function. otherwise return the Object passed in - // or default Object - const evalCacheOpts = (() => { - switch (typeof options.cacheControl) { - case 'function': - return options.cacheControl(request) - case 'object': - return options.cacheControl - default: - return defaultCacheControl - } - })() - - // formats the etag depending on the response context. if the entityId - // is invalid, returns an empty string (instead of null) to prevent the - // the potentially disastrous scenario where the value of the Etag resp - // header is "null". Could be modified in future to base64 encode etc - const formatETag = (entityId: any = pathKey, validatorType: string = options.defaultETag) => { - if (!entityId) { - return '' - } - switch (validatorType) { - case 'weak': - if (!entityId.startsWith('W/')) { - if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) { - return `W/${entityId}` - } - return `W/"${entityId}"` - } - return entityId - case 'strong': - if (entityId.startsWith(`W/"`)) { - entityId = entityId.replace('W/', '') - } - if (!entityId.endsWith(`"`)) { - entityId = `"${entityId}"` - } - return entityId - default: - return '' - } - } - - options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts) - - // override shouldEdgeCache if options say to bypassCache - if ( - options.cacheControl.bypassCache || - options.cacheControl.edgeTTL === null || - request.method == 'HEAD' - ) { - shouldEdgeCache = false - } - // only set max-age if explicitly passed in a number as an arg - const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number' - - let response = null - if (shouldEdgeCache) { - response = await cache.match(cacheKey) - } - - if (response) { - if (response.status > 300 && response.status < 400) { - if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) { - // Body exists and environment supports readable streams - response.body.cancel() - } else { - // Environment doesnt support readable streams, or null repsonse body. Nothing to do - } - response = new Response(null, response) - } else { - // fixes #165 - let opts = { - headers: new Headers(response.headers), - status: 0, - statusText: '', - } - - opts.headers.set('cf-cache-status', 'HIT') - - if (response.status) { - opts.status = response.status - opts.statusText = response.statusText - } else if (opts.headers.has('Content-Range')) { - opts.status = 206 - opts.statusText = 'Partial Content' - } else { - opts.status = 200 - opts.statusText = 'OK' - } - response = new Response(response.body, opts) - } - } else { - const body = await ASSET_NAMESPACE.get(pathKey, 'arrayBuffer') - if (body === null) { - throw new NotFoundError(`could not find ${pathKey} in your content namespace`) - } - response = new Response(body) - - if (shouldEdgeCache) { - response.headers.set('Accept-Ranges', 'bytes') - response.headers.set('Content-Length', String(body.byteLength)) - // set etag before cache insertion - if (!response.headers.has('etag')) { - response.headers.set('etag', formatETag(pathKey)) - } - // determine Cloudflare cache behavior - response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`) - event.waitUntil(cache.put(cacheKey, response.clone())) - response.headers.set('CF-Cache-Status', 'MISS') - } - } - response.headers.set('Content-Type', mimeType) - - if (response.status === 304) { - let etag = formatETag(response.headers.get('etag')) - let ifNoneMatch = cacheKey.headers.get('if-none-match') - let proxyCacheStatus = response.headers.get('CF-Cache-Status') - if (etag) { - if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === 'MISS') { - response.headers.set('CF-Cache-Status', 'EXPIRED') - } else { - response.headers.set('CF-Cache-Status', 'REVALIDATED') - } - response.headers.set('etag', formatETag(etag, 'weak')) - } - } - if (shouldSetBrowserCache) { - response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`) - } else { - response.headers.delete('Cache-Control') - } - return response + options = assignOptions(options) + + const request = event.request + const ASSET_NAMESPACE = options.ASSET_NAMESPACE + const ASSET_MANIFEST = parseStringAsObject(options.ASSET_MANIFEST) + + if (typeof ASSET_NAMESPACE === 'undefined') { + throw new InternalError(`there is no KV namespace bound to the script`) + } + + const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, '') // strip any preceding /'s + let pathIsEncoded = options.pathIsEncoded + let requestKey + // if options.mapRequestToAsset is explicitly passed in, always use it and assume user has own intentions + // otherwise handle request as normal, with default mapRequestToAsset below + if (options.mapRequestToAsset) { + requestKey = options.mapRequestToAsset(request) + } else if (ASSET_MANIFEST[rawPathKey]) { + requestKey = request + } else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) { + pathIsEncoded = true + requestKey = request + } else { + const mappedRequest = mapRequestToAsset(request) + const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, '') + if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) { + pathIsEncoded = true + requestKey = mappedRequest + } else { + // use default mapRequestToAsset + requestKey = mapRequestToAsset(request, options) + } + } + + const SUPPORTED_METHODS = ['GET', 'HEAD'] + if (!SUPPORTED_METHODS.includes(requestKey.method)) { + throw new MethodNotAllowedError(`${requestKey.method} is not a valid request method`) + } + + const parsedUrl = new URL(requestKey.url) + const pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary + + // pathKey is the file path to look up in the manifest + let pathKey = pathname.replace(/^\/+/, '') // remove prepended / + + // @ts-ignore + const cache = caches.default + let mimeType = mime.getType(pathKey) || options.defaultMimeType + if (mimeType.startsWith('text') || mimeType === 'application/javascript') { + mimeType += '; charset=utf-8' + } + + let shouldEdgeCache = false // false if storing in KV by raw file path i.e. no hash + // check manifest for map from file path to hash + if (typeof ASSET_MANIFEST !== 'undefined') { + if (ASSET_MANIFEST[pathKey]) { + pathKey = ASSET_MANIFEST[pathKey] + // if path key is in asset manifest, we can assume it contains a content hash and can be cached + shouldEdgeCache = true + } + } + + // TODO this excludes search params from cache, investigate ideal behavior + let cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request) + + // if argument passed in for cacheControl is a function then + // evaluate that function. otherwise return the Object passed in + // or default Object + const evalCacheOpts = (() => { + switch (typeof options.cacheControl) { + case 'function': + return options.cacheControl(request) + case 'object': + return options.cacheControl + default: + return defaultCacheControl + } + })() + + // formats the etag depending on the response context. if the entityId + // is invalid, returns an empty string (instead of null) to prevent the + // the potentially disastrous scenario where the value of the Etag resp + // header is "null". Could be modified in future to base64 encode etc + const formatETag = (entityId: any = pathKey, validatorType: string = options.defaultETag) => { + if (!entityId) { + return '' + } + switch (validatorType) { + case 'weak': + if (!entityId.startsWith('W/')) { + if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) { + return `W/${entityId}` + } + return `W/"${entityId}"` + } + return entityId + case 'strong': + if (entityId.startsWith(`W/"`)) { + entityId = entityId.replace('W/', '') + } + if (!entityId.endsWith(`"`)) { + entityId = `"${entityId}"` + } + return entityId + default: + return '' + } + } + + options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts) + + // override shouldEdgeCache if options say to bypassCache + if ( + options.cacheControl.bypassCache || + options.cacheControl.edgeTTL === null || + request.method == 'HEAD' + ) { + shouldEdgeCache = false + } + // only set max-age if explicitly passed in a number as an arg + const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number' + + let response = null + if (shouldEdgeCache) { + response = await cache.match(cacheKey) + } + + if (response) { + if (response.status > 300 && response.status < 400) { + if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) { + // Body exists and environment supports readable streams + response.body.cancel() + } else { + // Environment doesnt support readable streams, or null repsonse body. Nothing to do + } + response = new Response(null, response) + } else { + // fixes #165 + let opts = { + headers: new Headers(response.headers), + status: 0, + statusText: '', + } + + opts.headers.set('cf-cache-status', 'HIT') + + if (response.status) { + opts.status = response.status + opts.statusText = response.statusText + } else if (opts.headers.has('Content-Range')) { + opts.status = 206 + opts.statusText = 'Partial Content' + } else { + opts.status = 200 + opts.statusText = 'OK' + } + response = new Response(response.body, opts) + } + } else { + const body = await ASSET_NAMESPACE.get(pathKey, 'arrayBuffer') + if (body === null) { + throw new NotFoundError(`could not find ${pathKey} in your content namespace`) + } + response = new Response(body) + + if (shouldEdgeCache) { + response.headers.set('Accept-Ranges', 'bytes') + response.headers.set('Content-Length', String(body.byteLength)) + // set etag before cache insertion + if (!response.headers.has('etag')) { + response.headers.set('etag', formatETag(pathKey)) + } + // determine Cloudflare cache behavior + response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`) + event.waitUntil(cache.put(cacheKey, response.clone())) + response.headers.set('CF-Cache-Status', 'MISS') + } + } + response.headers.set('Content-Type', mimeType) + + if (response.status === 304) { + let etag = formatETag(response.headers.get('etag')) + let ifNoneMatch = cacheKey.headers.get('if-none-match') + let proxyCacheStatus = response.headers.get('CF-Cache-Status') + if (etag) { + if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === 'MISS') { + response.headers.set('CF-Cache-Status', 'EXPIRED') + } else { + response.headers.set('CF-Cache-Status', 'REVALIDATED') + } + response.headers.set('etag', formatETag(etag, 'weak')) + } + } + if (shouldSetBrowserCache) { + response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`) + } else { + response.headers.delete('Cache-Control') + } + return response } export { getAssetFromKV, mapRequestToAsset, serveSinglePageApp } diff --git a/src/mocks.ts b/src/mocks.ts index b42fb74..ad0830d 100644 --- a/src/mocks.ts +++ b/src/mocks.ts @@ -3,146 +3,146 @@ const makeServiceWorkerEnv = require('service-worker-mock') const HASH = '123HASHBROWN' export const getEvent = (request: Request): any => { - const waitUntil = async (callback: any) => { - await callback - } - return { - request, - waitUntil, - } + const waitUntil = async (callback: any) => { + await callback + } + return { + request, + waitUntil, + } } const store: any = { - 'key1.123HASHBROWN.txt': 'val1', - 'key1.123HASHBROWN.png': 'val1', - 'index.123HASHBROWN.html': 'index.html', - 'cache.123HASHBROWN.html': 'cache me if you can', - '测试.123HASHBROWN.html': 'My filename is non-ascii', - '%not-really-percent-encoded.123HASHBROWN.html': 'browser percent encoded', - '%2F.123HASHBROWN.html': 'user percent encoded', - '你好.123HASHBROWN.html': 'I shouldnt be served', - '%E4%BD%A0%E5%A5%BD.123HASHBROWN.html': 'Im important', - 'nohash.txt': 'no hash but still got some result', - 'sub/blah.123HASHBROWN.png': 'picturedis', - 'sub/index.123HASHBROWN.html': 'picturedis', - 'client.123HASHBROWN': 'important file', - 'client.123HASHBROWN/index.html': 'Im here but serve my big bro above', - 'image.123HASHBROWN.png': 'imagepng', - 'image.123HASHBROWN.webp': 'imagewebp', - '你好/index.123HASHBROWN.html': 'My path is non-ascii', + 'key1.123HASHBROWN.txt': 'val1', + 'key1.123HASHBROWN.png': 'val1', + 'index.123HASHBROWN.html': 'index.html', + 'cache.123HASHBROWN.html': 'cache me if you can', + '测试.123HASHBROWN.html': 'My filename is non-ascii', + '%not-really-percent-encoded.123HASHBROWN.html': 'browser percent encoded', + '%2F.123HASHBROWN.html': 'user percent encoded', + '你好.123HASHBROWN.html': 'I shouldnt be served', + '%E4%BD%A0%E5%A5%BD.123HASHBROWN.html': 'Im important', + 'nohash.txt': 'no hash but still got some result', + 'sub/blah.123HASHBROWN.png': 'picturedis', + 'sub/index.123HASHBROWN.html': 'picturedis', + 'client.123HASHBROWN': 'important file', + 'client.123HASHBROWN/index.html': 'Im here but serve my big bro above', + 'image.123HASHBROWN.png': 'imagepng', + 'image.123HASHBROWN.webp': 'imagewebp', + '你好/index.123HASHBROWN.html': 'My path is non-ascii', } export const mockKV = (store: any) => { - return { - get: (path: string) => store[path] || null, - } + return { + get: (path: string) => store[path] || null, + } } export const mockManifest = () => { - return JSON.stringify({ - 'key1.txt': `key1.${HASH}.txt`, - 'key1.png': `key1.${HASH}.png`, - 'cache.html': `cache.${HASH}.html`, - '测试.html': `测试.${HASH}.html`, - '你好.html': `你好.${HASH}.html`, - '%not-really-percent-encoded.html': `%not-really-percent-encoded.${HASH}.html`, - '%2F.html': `%2F.${HASH}.html`, - '%E4%BD%A0%E5%A5%BD.html': `%E4%BD%A0%E5%A5%BD.${HASH}.html`, - 'index.html': `index.${HASH}.html`, - 'sub/blah.png': `sub/blah.${HASH}.png`, - 'sub/index.html': `sub/index.${HASH}.html`, - client: `client.${HASH}`, - 'client/index.html': `client.${HASH}`, - 'image.png': `image.${HASH}.png`, - 'image.webp': `image.${HASH}.webp`, - '你好/index.html': `你好/index.${HASH}.html`, - }) + return JSON.stringify({ + 'key1.txt': `key1.${HASH}.txt`, + 'key1.png': `key1.${HASH}.png`, + 'cache.html': `cache.${HASH}.html`, + '测试.html': `测试.${HASH}.html`, + '你好.html': `你好.${HASH}.html`, + '%not-really-percent-encoded.html': `%not-really-percent-encoded.${HASH}.html`, + '%2F.html': `%2F.${HASH}.html`, + '%E4%BD%A0%E5%A5%BD.html': `%E4%BD%A0%E5%A5%BD.${HASH}.html`, + 'index.html': `index.${HASH}.html`, + 'sub/blah.png': `sub/blah.${HASH}.png`, + 'sub/index.html': `sub/index.${HASH}.html`, + client: `client.${HASH}`, + 'client/index.html': `client.${HASH}`, + 'image.png': `image.${HASH}.png`, + 'image.webp': `image.${HASH}.webp`, + '你好/index.html': `你好/index.${HASH}.html`, + }) } let cacheStore: any = new Map() interface CacheKey { - url: object - headers: object + url: object + headers: object } export const mockCaches = () => { - return { - default: { - async match(key: any) { - let cacheKey: CacheKey = { - url: key.url, - headers: {}, - } - let response - if (key.headers.has('if-none-match')) { - let makeStrongEtag = key.headers.get('if-none-match').replace('W/', '') - Reflect.set(cacheKey.headers, 'etag', makeStrongEtag) - response = cacheStore.get(JSON.stringify(cacheKey)) - } else { - // if client doesn't send if-none-match, we need to iterate through these keys - // and just test the URL - const activeCacheKeys: Array = Array.from(cacheStore.keys()) - for (const cacheStoreKey of activeCacheKeys) { - if (JSON.parse(cacheStoreKey).url === key.url) { - response = cacheStore.get(cacheStoreKey) - } - } - } - // TODO: write test to accomodate for rare scenarios with where range requests accomodate etags - if (response && !key.headers.has('if-none-match')) { - // this appears overly verbose, but is necessary to document edge cache behavior - // The Range request header triggers the response header Content-Range ... - const range = key.headers.get('range') - if (range) { - response.headers.set( - 'content-range', - `bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`, - ) - } - // ... which we are using in this repository to set status 206 - if (response.headers.has('content-range')) { - response.status = 206 - } else { - response.status = 200 - } - let etag = response.headers.get('etag') - if (etag && !etag.includes('W/')) { - response.headers.set('etag', `W/${etag}`) - } - } - return response - }, - async put(key: any, val: Response) { - let headers = new Headers(val.headers) - let url = new URL(key.url) - let resWithBody = new Response(val.body, { headers, status: 200 }) - let resNoBody = new Response(null, { headers, status: 304 }) - let cacheKey: CacheKey = { - url: key.url, - headers: { - etag: `"${url.pathname.replace('/', '')}"`, - }, - } - cacheStore.set(JSON.stringify(cacheKey), resNoBody) - cacheKey.headers = {} - cacheStore.set(JSON.stringify(cacheKey), resWithBody) - return - }, - }, - } + return { + default: { + async match(key: any) { + let cacheKey: CacheKey = { + url: key.url, + headers: {}, + } + let response + if (key.headers.has('if-none-match')) { + let makeStrongEtag = key.headers.get('if-none-match').replace('W/', '') + Reflect.set(cacheKey.headers, 'etag', makeStrongEtag) + response = cacheStore.get(JSON.stringify(cacheKey)) + } else { + // if client doesn't send if-none-match, we need to iterate through these keys + // and just test the URL + const activeCacheKeys: Array = Array.from(cacheStore.keys()) + for (const cacheStoreKey of activeCacheKeys) { + if (JSON.parse(cacheStoreKey).url === key.url) { + response = cacheStore.get(cacheStoreKey) + } + } + } + // TODO: write test to accomodate for rare scenarios with where range requests accomodate etags + if (response && !key.headers.has('if-none-match')) { + // this appears overly verbose, but is necessary to document edge cache behavior + // The Range request header triggers the response header Content-Range ... + const range = key.headers.get('range') + if (range) { + response.headers.set( + 'content-range', + `bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`, + ) + } + // ... which we are using in this repository to set status 206 + if (response.headers.has('content-range')) { + response.status = 206 + } else { + response.status = 200 + } + let etag = response.headers.get('etag') + if (etag && !etag.includes('W/')) { + response.headers.set('etag', `W/${etag}`) + } + } + return response + }, + async put(key: any, val: Response) { + let headers = new Headers(val.headers) + let url = new URL(key.url) + let resWithBody = new Response(val.body, { headers, status: 200 }) + let resNoBody = new Response(null, { headers, status: 304 }) + let cacheKey: CacheKey = { + url: key.url, + headers: { + etag: `"${url.pathname.replace('/', '')}"`, + }, + } + cacheStore.set(JSON.stringify(cacheKey), resNoBody) + cacheKey.headers = {} + cacheStore.set(JSON.stringify(cacheKey), resWithBody) + return + }, + }, + } } // mocks functionality used inside worker request export function mockRequestScope() { - Object.assign(global, makeServiceWorkerEnv()) - Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() }) - Object.assign(global, { __STATIC_CONTENT: mockKV(store) }) - Object.assign(global, { caches: mockCaches() }) + Object.assign(global, makeServiceWorkerEnv()) + Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() }) + Object.assign(global, { __STATIC_CONTENT: mockKV(store) }) + Object.assign(global, { caches: mockCaches() }) } // mocks functionality used on global isolate scope. such as the KV namespace bind export function mockGlobalScope() { - Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() }) - Object.assign(global, { __STATIC_CONTENT: mockKV(store) }) + Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() }) + Object.assign(global, { __STATIC_CONTENT: mockKV(store) }) } export const sleep = (milliseconds: number) => { - return new Promise((resolve) => setTimeout(resolve, milliseconds)) + return new Promise((resolve) => setTimeout(resolve, milliseconds)) } diff --git a/src/test/getAssetFromKV-optional.ts b/src/test/getAssetFromKV-optional.ts index 9740b7d..afe956a 100644 --- a/src/test/getAssetFromKV-optional.ts +++ b/src/test/getAssetFromKV-optional.ts @@ -8,17 +8,17 @@ Object.assign(global, { __STATIC_CONTENT_MANIFEST: undefined }) import { getAssetFromKV, mapRequestToAsset } from '../index' test('getAssetFromKV return correct val from KV without manifest', async (t) => { - mockRequestScope() - // manually reset manifest global, to test optional behaviour - Object.assign(global, { __STATIC_CONTENT_MANIFEST: undefined }) + mockRequestScope() + // manually reset manifest global, to test optional behaviour + Object.assign(global, { __STATIC_CONTENT_MANIFEST: undefined }) - const event = getEvent(new Request('https://blah.com/key1.123HASHBROWN.txt')) - const res = await getAssetFromKV(event) + const event = getEvent(new Request('https://blah.com/key1.123HASHBROWN.txt')) + const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'val1') - t.true(res.headers.get('content-type').includes('text')) - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(await res.text(), 'val1') + t.true(res.headers.get('content-type').includes('text')) + } else { + t.fail('Response was undefined') + } }) diff --git a/src/test/getAssetFromKV.ts b/src/test/getAssetFromKV.ts index 71f7d77..0844a00 100644 --- a/src/test/getAssetFromKV.ts +++ b/src/test/getAssetFromKV.ts @@ -6,519 +6,519 @@ import { getAssetFromKV, mapRequestToAsset } from '../index' import { KVError } from '../types' test('getAssetFromKV return correct val from KV and default caching', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/key1.txt')) - const res = await getAssetFromKV(event) - - if (res) { - t.is(res.headers.get('cache-control'), null) - t.is(res.headers.get('cf-cache-status'), 'MISS') - t.is(await res.text(), 'val1') - t.true(res.headers.get('content-type').includes('text')) - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const event = getEvent(new Request('https://blah.com/key1.txt')) + const res = await getAssetFromKV(event) + + if (res) { + t.is(res.headers.get('cache-control'), null) + t.is(res.headers.get('cf-cache-status'), 'MISS') + t.is(await res.text(), 'val1') + t.true(res.headers.get('content-type').includes('text')) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV evaluated the file matching the extensionless path first /client/ -> client', async (t) => { - mockRequestScope() - const event = getEvent(new Request(`https://foo.com/client/`)) - const res = await getAssetFromKV(event) - t.is(await res.text(), 'important file') - t.true(res.headers.get('content-type').includes('text')) + mockRequestScope() + const event = getEvent(new Request(`https://foo.com/client/`)) + const res = await getAssetFromKV(event) + t.is(await res.text(), 'important file') + t.true(res.headers.get('content-type').includes('text')) }) test('getAssetFromKV evaluated the file matching the extensionless path first /client -> client', async (t) => { - mockRequestScope() - const event = getEvent(new Request(`https://foo.com/client`)) - const res = await getAssetFromKV(event) - t.is(await res.text(), 'important file') - t.true(res.headers.get('content-type').includes('text')) + mockRequestScope() + const event = getEvent(new Request(`https://foo.com/client`)) + const res = await getAssetFromKV(event) + t.is(await res.text(), 'important file') + t.true(res.headers.get('content-type').includes('text')) }) test('getAssetFromKV if not in asset manifest still returns nohash.txt', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/nohash.txt')) - const res = await getAssetFromKV(event) + mockRequestScope() + const event = getEvent(new Request('https://blah.com/nohash.txt')) + const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'no hash but still got some result') - t.true(res.headers.get('content-type').includes('text')) - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(await res.text(), 'no hash but still got some result') + t.true(res.headers.get('content-type').includes('text')) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV if no asset manifest /client -> client fails', async (t) => { - mockRequestScope() - const event = getEvent(new Request(`https://foo.com/client`)) - const error: KVError = await t.throwsAsync(getAssetFromKV(event, { ASSET_MANIFEST: {} })) - t.is(error.status, 404) + mockRequestScope() + const event = getEvent(new Request(`https://foo.com/client`)) + const error: KVError = await t.throwsAsync(getAssetFromKV(event, { ASSET_MANIFEST: {} })) + t.is(error.status, 404) }) test('getAssetFromKV if sub/ -> sub/index.html served', async (t) => { - mockRequestScope() - const event = getEvent(new Request(`https://foo.com/sub`)) - const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'picturedis') - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const event = getEvent(new Request(`https://foo.com/sub`)) + const res = await getAssetFromKV(event) + if (res) { + t.is(await res.text(), 'picturedis') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV gets index.html by default for / requests', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/')) - const res = await getAssetFromKV(event) + mockRequestScope() + const event = getEvent(new Request('https://blah.com/')) + const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'index.html') - t.true(res.headers.get('content-type').includes('html')) - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(await res.text(), 'index.html') + t.true(res.headers.get('content-type').includes('html')) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV non ASCII path support', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/测试.html')) - const res = await getAssetFromKV(event) + mockRequestScope() + const event = getEvent(new Request('https://blah.com/测试.html')) + const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'My filename is non-ascii') - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(await res.text(), 'My filename is non-ascii') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV supports browser percent encoded URLs', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://example.com/%not-really-percent-encoded.html')) - const res = await getAssetFromKV(event) + mockRequestScope() + const event = getEvent(new Request('https://example.com/%not-really-percent-encoded.html')) + const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'browser percent encoded') - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(await res.text(), 'browser percent encoded') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV supports user percent encoded URLs', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/%2F.html')) - const res = await getAssetFromKV(event) + mockRequestScope() + const event = getEvent(new Request('https://blah.com/%2F.html')) + const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'user percent encoded') - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(await res.text(), 'user percent encoded') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV only decode URL when necessary', async (t) => { - mockRequestScope() - const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD.html')) - const event2 = getEvent(new Request('https://blah.com/你好.html')) - const res1 = await getAssetFromKV(event1) - const res2 = await getAssetFromKV(event2) + mockRequestScope() + const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD.html')) + const event2 = getEvent(new Request('https://blah.com/你好.html')) + const res1 = await getAssetFromKV(event1) + const res2 = await getAssetFromKV(event2) - if (res1 && res2) { - t.is(await res1.text(), 'Im important') - t.is(await res2.text(), 'Im important') - } else { - t.fail('Response was undefined') - } + if (res1 && res2) { + t.is(await res1.text(), 'Im important') + t.is(await res2.text(), 'Im important') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV Support for user decode url path', async (t) => { - mockRequestScope() - const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD/')) - const event2 = getEvent(new Request('https://blah.com/你好/')) - const res1 = await getAssetFromKV(event1) - const res2 = await getAssetFromKV(event2) + mockRequestScope() + const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD/')) + const event2 = getEvent(new Request('https://blah.com/你好/')) + const res1 = await getAssetFromKV(event1) + const res2 = await getAssetFromKV(event2) - if (res1 && res2) { - t.is(await res1.text(), 'My path is non-ascii') - t.is(await res2.text(), 'My path is non-ascii') - } else { - t.fail('Response was undefined') - } + if (res1 && res2) { + t.is(await res1.text(), 'My path is non-ascii') + t.is(await res2.text(), 'My path is non-ascii') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV custom key modifier', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/docs/sub/blah.png')) + mockRequestScope() + const event = getEvent(new Request('https://blah.com/docs/sub/blah.png')) - const customRequestMapper = (request: Request) => { - let defaultModifiedRequest = mapRequestToAsset(request) + const customRequestMapper = (request: Request) => { + let defaultModifiedRequest = mapRequestToAsset(request) - let url = new URL(defaultModifiedRequest.url) - url.pathname = url.pathname.replace('/docs', '') - return new Request(url.toString(), request) - } + let url = new URL(defaultModifiedRequest.url) + url.pathname = url.pathname.replace('/docs', '') + return new Request(url.toString(), request) + } - const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper }) + const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper }) - if (res) { - t.is(await res.text(), 'picturedis') - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(await res.text(), 'picturedis') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV request override with existing manifest file', async (t) => { - // see https://github.com/cloudflare/kv-asset-handler/pull/159 for more info - mockRequestScope() - const event = getEvent(new Request('https://blah.com/image.png')) // real file in manifest + // see https://github.com/cloudflare/kv-asset-handler/pull/159 for more info + mockRequestScope() + const event = getEvent(new Request('https://blah.com/image.png')) // real file in manifest - const customRequestMapper = (request: Request) => { - let defaultModifiedRequest = mapRequestToAsset(request) + const customRequestMapper = (request: Request) => { + let defaultModifiedRequest = mapRequestToAsset(request) - let url = new URL(defaultModifiedRequest.url) - url.pathname = '/image.webp' // other different file in manifest - return new Request(url.toString(), request) - } + let url = new URL(defaultModifiedRequest.url) + url.pathname = '/image.webp' // other different file in manifest + return new Request(url.toString(), request) + } - const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper }) + const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper }) - if (res) { - t.is(await res.text(), 'imagewebp') - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(await res.text(), 'imagewebp') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV when setting browser caching', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/')) + mockRequestScope() + const event = getEvent(new Request('https://blah.com/')) - const res = await getAssetFromKV(event, { cacheControl: { browserTTL: 22 } }) + const res = await getAssetFromKV(event, { cacheControl: { browserTTL: 22 } }) - if (res) { - t.is(res.headers.get('cache-control'), 'max-age=22') - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(res.headers.get('cache-control'), 'max-age=22') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV when setting custom cache setting', async (t) => { - mockRequestScope() - const event1 = getEvent(new Request('https://blah.com/')) - const event2 = getEvent(new Request('https://blah.com/key1.png?blah=34')) - const cacheOnlyPngs = (req: Request) => { - if (new URL(req.url).pathname.endsWith('.png')) - return { - browserTTL: 720, - edgeTTL: 720, - } - else - return { - bypassCache: true, - } - } - - const res1 = await getAssetFromKV(event1, { cacheControl: cacheOnlyPngs }) - const res2 = await getAssetFromKV(event2, { cacheControl: cacheOnlyPngs }) - - if (res1 && res2) { - t.is(res1.headers.get('cache-control'), null) - t.true(res2.headers.get('content-type').includes('png')) - t.is(res2.headers.get('cache-control'), 'max-age=720') - t.is(res2.headers.get('cf-cache-status'), 'MISS') - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const event1 = getEvent(new Request('https://blah.com/')) + const event2 = getEvent(new Request('https://blah.com/key1.png?blah=34')) + const cacheOnlyPngs = (req: Request) => { + if (new URL(req.url).pathname.endsWith('.png')) + return { + browserTTL: 720, + edgeTTL: 720, + } + else + return { + bypassCache: true, + } + } + + const res1 = await getAssetFromKV(event1, { cacheControl: cacheOnlyPngs }) + const res2 = await getAssetFromKV(event2, { cacheControl: cacheOnlyPngs }) + + if (res1 && res2) { + t.is(res1.headers.get('cache-control'), null) + t.true(res2.headers.get('content-type').includes('png')) + t.is(res2.headers.get('cache-control'), 'max-age=720') + t.is(res2.headers.get('cf-cache-status'), 'MISS') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV caches on two sequential requests', async (t) => { - mockRequestScope() - const resourceKey = 'cache.html' - const resourceVersion = JSON.parse(mockManifest())[resourceKey] - const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) - const event2 = getEvent( - new Request(`https://blah.com/${resourceKey}`, { - headers: { - 'if-none-match': `"${resourceVersion}"`, - }, - }), - ) - - const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720, browserTTL: 720 } }) - await sleep(1) - const res2 = await getAssetFromKV(event2) - - if (res1 && res2) { - t.is(res1.headers.get('cf-cache-status'), 'MISS') - t.is(res1.headers.get('cache-control'), 'max-age=720') - t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const resourceKey = 'cache.html' + const resourceVersion = JSON.parse(mockManifest())[resourceKey] + const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) + const event2 = getEvent( + new Request(`https://blah.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}"`, + }, + }), + ) + + const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720, browserTTL: 720 } }) + await sleep(1) + const res2 = await getAssetFromKV(event2) + + if (res1 && res2) { + t.is(res1.headers.get('cf-cache-status'), 'MISS') + t.is(res1.headers.get('cache-control'), 'max-age=720') + t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV does not store max-age on two sequential requests', async (t) => { - mockRequestScope() - const resourceKey = 'cache.html' - const resourceVersion = JSON.parse(mockManifest())[resourceKey] - const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) - const event2 = getEvent( - new Request(`https://blah.com/${resourceKey}`, { - headers: { - 'if-none-match': `"${resourceVersion}"`, - }, - }), - ) - - const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) - await sleep(100) - const res2 = await getAssetFromKV(event2) - - if (res1 && res2) { - t.is(res1.headers.get('cf-cache-status'), 'MISS') - t.is(res1.headers.get('cache-control'), null) - t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') - t.is(res2.headers.get('cache-control'), null) - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const resourceKey = 'cache.html' + const resourceVersion = JSON.parse(mockManifest())[resourceKey] + const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) + const event2 = getEvent( + new Request(`https://blah.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}"`, + }, + }), + ) + + const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) + await sleep(100) + const res2 = await getAssetFromKV(event2) + + if (res1 && res2) { + t.is(res1.headers.get('cf-cache-status'), 'MISS') + t.is(res1.headers.get('cache-control'), null) + t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') + t.is(res2.headers.get('cache-control'), null) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV does not cache on Cloudflare when bypass cache set', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/')) + mockRequestScope() + const event = getEvent(new Request('https://blah.com/')) - const res = await getAssetFromKV(event, { cacheControl: { bypassCache: true } }) + const res = await getAssetFromKV(event, { cacheControl: { bypassCache: true } }) - if (res) { - t.is(res.headers.get('cache-control'), null) - t.is(res.headers.get('cf-cache-status'), null) - } else { - t.fail('Response was undefined') - } + if (res) { + t.is(res.headers.get('cache-control'), null) + t.is(res.headers.get('cf-cache-status'), null) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV with no trailing slash on root', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com')) - const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'index.html') - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const event = getEvent(new Request('https://blah.com')) + const res = await getAssetFromKV(event) + if (res) { + t.is(await res.text(), 'index.html') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV with no trailing slash on a subdirectory', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/sub/blah.png')) - const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'picturedis') - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const event = getEvent(new Request('https://blah.com/sub/blah.png')) + const res = await getAssetFromKV(event) + if (res) { + t.is(await res.text(), 'picturedis') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV no result throws an error', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/random')) - const error: KVError = await t.throwsAsync(getAssetFromKV(event)) - t.is(error.status, 404) + mockRequestScope() + const event = getEvent(new Request('https://blah.com/random')) + const error: KVError = await t.throwsAsync(getAssetFromKV(event)) + t.is(error.status, 404) }) test('getAssetFromKV TTls set to null should not cache on browser or edge', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/')) - - const res1 = await getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } }) - await sleep(100) - const res2 = await getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } }) - - if (res1 && res2) { - t.is(res1.headers.get('cf-cache-status'), null) - t.is(res1.headers.get('cache-control'), null) - t.is(res2.headers.get('cf-cache-status'), null) - t.is(res2.headers.get('cache-control'), null) - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const event = getEvent(new Request('https://blah.com/')) + + const res1 = await getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } }) + await sleep(100) + const res2 = await getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } }) + + if (res1 && res2) { + t.is(res1.headers.get('cf-cache-status'), null) + t.is(res1.headers.get('cache-control'), null) + t.is(res2.headers.get('cf-cache-status'), null) + t.is(res2.headers.get('cache-control'), null) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV passing in a custom NAMESPACE serves correct asset', async (t) => { - mockRequestScope() - let CUSTOM_NAMESPACE = mockKV({ - 'key1.123HASHBROWN.txt': 'val1', - }) - Object.assign(global, { CUSTOM_NAMESPACE }) - const event = getEvent(new Request('https://blah.com/')) - const res = await getAssetFromKV(event) - if (res) { - t.is(await res.text(), 'index.html') - t.true(res.headers.get('content-type').includes('html')) - } else { - t.fail('Response was undefined') - } + mockRequestScope() + let CUSTOM_NAMESPACE = mockKV({ + 'key1.123HASHBROWN.txt': 'val1', + }) + Object.assign(global, { CUSTOM_NAMESPACE }) + const event = getEvent(new Request('https://blah.com/')) + const res = await getAssetFromKV(event) + if (res) { + t.is(await res.text(), 'index.html') + t.true(res.headers.get('content-type').includes('html')) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV when custom namespace without the asset should fail', async (t) => { - mockRequestScope() - let CUSTOM_NAMESPACE = mockKV({ - 'key5.123HASHBROWN.txt': 'customvalu', - }) + mockRequestScope() + let CUSTOM_NAMESPACE = mockKV({ + 'key5.123HASHBROWN.txt': 'customvalu', + }) - const event = getEvent(new Request('https://blah.com')) - const error: KVError = await t.throwsAsync( - getAssetFromKV(event, { ASSET_NAMESPACE: CUSTOM_NAMESPACE }), - ) - t.is(error.status, 404) + const event = getEvent(new Request('https://blah.com')) + const error: KVError = await t.throwsAsync( + getAssetFromKV(event, { ASSET_NAMESPACE: CUSTOM_NAMESPACE }), + ) + t.is(error.status, 404) }) test('getAssetFromKV when namespace not bound fails', async (t) => { - mockRequestScope() - var MY_CUSTOM_NAMESPACE = undefined - Object.assign(global, { MY_CUSTOM_NAMESPACE }) + mockRequestScope() + var MY_CUSTOM_NAMESPACE = undefined + Object.assign(global, { MY_CUSTOM_NAMESPACE }) - const event = getEvent(new Request('https://blah.com/')) - const error: KVError = await t.throwsAsync( - getAssetFromKV(event, { ASSET_NAMESPACE: MY_CUSTOM_NAMESPACE }), - ) - t.is(error.status, 500) + const event = getEvent(new Request('https://blah.com/')) + const error: KVError = await t.throwsAsync( + getAssetFromKV(event, { ASSET_NAMESPACE: MY_CUSTOM_NAMESPACE }), + ) + t.is(error.status, 500) }) test('getAssetFromKV when if-none-match === active resource version, should revalidate', async (t) => { - mockRequestScope() - const resourceKey = 'key1.png' - const resourceVersion = JSON.parse(mockManifest())[resourceKey] - const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) - const event2 = getEvent( - new Request(`https://blah.com/${resourceKey}`, { - headers: { - 'if-none-match': `W/"${resourceVersion}"`, - }, - }), - ) - - const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) - await sleep(100) - const res2 = await getAssetFromKV(event2) - - if (res1 && res2) { - t.is(res1.headers.get('cf-cache-status'), 'MISS') - t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const resourceKey = 'key1.png' + const resourceVersion = JSON.parse(mockManifest())[resourceKey] + const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) + const event2 = getEvent( + new Request(`https://blah.com/${resourceKey}`, { + headers: { + 'if-none-match': `W/"${resourceVersion}"`, + }, + }), + ) + + const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) + await sleep(100) + const res2 = await getAssetFromKV(event2) + + if (res1 && res2) { + t.is(res1.headers.get('cf-cache-status'), 'MISS') + t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV when if-none-match equals etag of stale resource then should bypass cache', async (t) => { - mockRequestScope() - const resourceKey = 'key1.png' - const resourceVersion = JSON.parse(mockManifest())[resourceKey] - const req1 = new Request(`https://blah.com/${resourceKey}`, { - headers: { - 'if-none-match': `"${resourceVersion}"`, - }, - }) - const req2 = new Request(`https://blah.com/${resourceKey}`, { - headers: { - 'if-none-match': `"${resourceVersion}-another-version"`, - }, - }) - const event = getEvent(req1) - const event2 = getEvent(req2) - const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }) - const res2 = await getAssetFromKV(event) - const res3 = await getAssetFromKV(event2) - if (res1 && res2 && res3) { - t.is(res1.headers.get('cf-cache-status'), 'MISS') - t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`) - t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') - t.not(res3.headers.get('etag'), req2.headers.get('if-none-match')) - t.is(res3.headers.get('cf-cache-status'), 'MISS') - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const resourceKey = 'key1.png' + const resourceVersion = JSON.parse(mockManifest())[resourceKey] + const req1 = new Request(`https://blah.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}"`, + }, + }) + const req2 = new Request(`https://blah.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}-another-version"`, + }, + }) + const event = getEvent(req1) + const event2 = getEvent(req2) + const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }) + const res2 = await getAssetFromKV(event) + const res3 = await getAssetFromKV(event2) + if (res1 && res2 && res3) { + t.is(res1.headers.get('cf-cache-status'), 'MISS') + t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`) + t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') + t.not(res3.headers.get('etag'), req2.headers.get('if-none-match')) + t.is(res3.headers.get('cf-cache-status'), 'MISS') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV when resource in cache, etag should be weakened before returned to eyeball', async (t) => { - mockRequestScope() - const resourceKey = 'key1.png' - const resourceVersion = JSON.parse(mockManifest())[resourceKey] - const req1 = new Request(`https://blah.com/${resourceKey}`, { - headers: { - 'if-none-match': `"${resourceVersion}"`, - }, - }) - const event = getEvent(req1) - const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }) - const res2 = await getAssetFromKV(event) - if (res1 && res2) { - t.is(res1.headers.get('cf-cache-status'), 'MISS') - t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`) - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const resourceKey = 'key1.png' + const resourceVersion = JSON.parse(mockManifest())[resourceKey] + const req1 = new Request(`https://blah.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}"`, + }, + }) + const event = getEvent(req1) + const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }) + const res2 = await getAssetFromKV(event) + if (res1 && res2) { + t.is(res1.headers.get('cf-cache-status'), 'MISS') + t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV should support weak etag override of resource', async (t) => { - mockRequestScope() - const resourceKey = 'key1.png' - const resourceVersion = JSON.parse(mockManifest())[resourceKey] - const req1 = new Request(`https://blah-weak.com/${resourceKey}`, { - headers: { - 'if-none-match': `W/"${resourceVersion}"`, - }, - }) - const req2 = new Request(`https://blah-weak.com/${resourceKey}`, { - headers: { - 'if-none-match': `"${resourceVersion}"`, - }, - }) - const req3 = new Request(`https://blah-weak.com/${resourceKey}`, { - headers: { - 'if-none-match': `"${resourceVersion}-another-version"`, - }, - }) - const event1 = getEvent(req1) - const event2 = getEvent(req2) - const event3 = getEvent(req3) - const res1 = await getAssetFromKV(event1, { defaultETag: 'weak' }) - const res2 = await getAssetFromKV(event2, { defaultETag: 'weak' }) - const res3 = await getAssetFromKV(event3, { defaultETag: 'weak' }) - if (res1 && res2 && res3) { - t.is(res1.headers.get('cf-cache-status'), 'MISS') - t.is(res1.headers.get('etag'), req1.headers.get('if-none-match')) - t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') - t.is(res2.headers.get('etag'), `W/${req2.headers.get('if-none-match')}`) - t.is(res3.headers.get('cf-cache-status'), 'MISS') - t.not(res3.headers.get('etag'), req2.headers.get('if-none-match')) - } else { - t.fail('Response was undefined') - } + mockRequestScope() + const resourceKey = 'key1.png' + const resourceVersion = JSON.parse(mockManifest())[resourceKey] + const req1 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + 'if-none-match': `W/"${resourceVersion}"`, + }, + }) + const req2 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}"`, + }, + }) + const req3 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}-another-version"`, + }, + }) + const event1 = getEvent(req1) + const event2 = getEvent(req2) + const event3 = getEvent(req3) + const res1 = await getAssetFromKV(event1, { defaultETag: 'weak' }) + const res2 = await getAssetFromKV(event2, { defaultETag: 'weak' }) + const res3 = await getAssetFromKV(event3, { defaultETag: 'weak' }) + if (res1 && res2 && res3) { + t.is(res1.headers.get('cf-cache-status'), 'MISS') + t.is(res1.headers.get('etag'), req1.headers.get('if-none-match')) + t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') + t.is(res2.headers.get('etag'), `W/${req2.headers.get('if-none-match')}`) + t.is(res3.headers.get('cf-cache-status'), 'MISS') + t.not(res3.headers.get('etag'), req2.headers.get('if-none-match')) + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV if-none-match not sent but resource in cache, should return cache hit 200 OK', async (t) => { - const resourceKey = 'cache.html' - const event = getEvent(new Request(`https://blah.com/${resourceKey}`)) - const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }) - await sleep(1) - const res2 = await getAssetFromKV(event) - if (res1 && res2) { - t.is(res1.headers.get('cf-cache-status'), 'MISS') - t.is(res1.headers.get('cache-control'), null) - t.is(res2.status, 200) - t.is(res2.headers.get('cf-cache-status'), 'HIT') - } else { - t.fail('Response was undefined') - } + const resourceKey = 'cache.html' + const event = getEvent(new Request(`https://blah.com/${resourceKey}`)) + const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }) + await sleep(1) + const res2 = await getAssetFromKV(event) + if (res1 && res2) { + t.is(res1.headers.get('cf-cache-status'), 'MISS') + t.is(res1.headers.get('cache-control'), null) + t.is(res2.status, 200) + t.is(res2.headers.get('cf-cache-status'), 'HIT') + } else { + t.fail('Response was undefined') + } }) test('getAssetFromKV if range request submitted and resource in cache, request fulfilled', async (t) => { - const resourceKey = 'cache.html' - const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) - const event2 = getEvent( - new Request(`https://blah.com/${resourceKey}`, { headers: { range: 'bytes=0-10' } }), - ) - const res1 = getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) - await res1 - await sleep(2) - const res2 = await getAssetFromKV(event2) - if (res2.headers.has('content-range')) { - t.is(res2.status, 206) - } else { - t.fail('Response was undefined') - } + const resourceKey = 'cache.html' + const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`)) + const event2 = getEvent( + new Request(`https://blah.com/${resourceKey}`, { headers: { range: 'bytes=0-10' } }), + ) + const res1 = getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) + await res1 + await sleep(2) + const res2 = await getAssetFromKV(event2) + if (res2.headers.has('content-range')) { + t.is(res2.status, 206) + } else { + t.fail('Response was undefined') + } }) test.todo('getAssetFromKV when body not empty, should invoke .cancel()') diff --git a/src/test/mapRequestToAsset.ts b/src/test/mapRequestToAsset.ts index 422165e..94e37fe 100644 --- a/src/test/mapRequestToAsset.ts +++ b/src/test/mapRequestToAsset.ts @@ -5,33 +5,33 @@ mockGlobalScope() import { mapRequestToAsset } from '../index' test('mapRequestToAsset() correctly changes /about -> /about/index.html', async (t) => { - mockRequestScope() - let path = '/about' - let request = new Request(`https://foo.com${path}`) - let newRequest = mapRequestToAsset(request) - t.is(newRequest.url, request.url + '/index.html') + mockRequestScope() + let path = '/about' + let request = new Request(`https://foo.com${path}`) + let newRequest = mapRequestToAsset(request) + t.is(newRequest.url, request.url + '/index.html') }) test('mapRequestToAsset() correctly changes /about/ -> /about/index.html', async (t) => { - mockRequestScope() - let path = '/about/' - let request = new Request(`https://foo.com${path}`) - let newRequest = mapRequestToAsset(request) - t.is(newRequest.url, request.url + 'index.html') + mockRequestScope() + let path = '/about/' + let request = new Request(`https://foo.com${path}`) + let newRequest = mapRequestToAsset(request) + t.is(newRequest.url, request.url + 'index.html') }) test('mapRequestToAsset() correctly changes /about.me/ -> /about.me/index.html', async (t) => { - mockRequestScope() - let path = '/about.me/' - let request = new Request(`https://foo.com${path}`) - let newRequest = mapRequestToAsset(request) - t.is(newRequest.url, request.url + 'index.html') + mockRequestScope() + let path = '/about.me/' + let request = new Request(`https://foo.com${path}`) + let newRequest = mapRequestToAsset(request) + t.is(newRequest.url, request.url + 'index.html') }) test('mapRequestToAsset() correctly changes /about -> /about/default.html', async (t) => { - mockRequestScope() - let path = '/about' - let request = new Request(`https://foo.com${path}`) - let newRequest = mapRequestToAsset(request, { defaultDocument: 'default.html' }) - t.is(newRequest.url, request.url + '/default.html') + mockRequestScope() + let path = '/about' + let request = new Request(`https://foo.com${path}`) + let newRequest = mapRequestToAsset(request, { defaultDocument: 'default.html' }) + t.is(newRequest.url, request.url + '/default.html') }) diff --git a/src/test/serveSinglePageApp.ts b/src/test/serveSinglePageApp.ts index 6c634c8..b299fcf 100644 --- a/src/test/serveSinglePageApp.ts +++ b/src/test/serveSinglePageApp.ts @@ -5,40 +5,40 @@ mockGlobalScope() import { serveSinglePageApp } from '../index' function testRequest(path: string) { - mockRequestScope() - let url = new URL('https://example.com') - url.pathname = path - let request = new Request(url.toString()) + mockRequestScope() + let url = new URL('https://example.com') + url.pathname = path + let request = new Request(url.toString()) - return request + return request } test('serveSinglePageApp returns root asset path when request path ends in .html', async (t) => { - let path = '/foo/thing.html' - let request = testRequest(path) + let path = '/foo/thing.html' + let request = testRequest(path) - let expected_request = testRequest('/index.html') - let actual_request = serveSinglePageApp(request) + let expected_request = testRequest('/index.html') + let actual_request = serveSinglePageApp(request) - t.deepEqual(expected_request, actual_request) + t.deepEqual(expected_request, actual_request) }) test('serveSinglePageApp returns root asset path when request path does not have extension', async (t) => { - let path = '/foo/thing' - let request = testRequest(path) + let path = '/foo/thing' + let request = testRequest(path) - let expected_request = testRequest('/index.html') - let actual_request = serveSinglePageApp(request) + let expected_request = testRequest('/index.html') + let actual_request = serveSinglePageApp(request) - t.deepEqual(expected_request, actual_request) + t.deepEqual(expected_request, actual_request) }) test('serveSinglePageApp returns requested asset when request path has non-html extension', async (t) => { - let path = '/foo/thing.js' - let request = testRequest(path) + let path = '/foo/thing.js' + let request = testRequest(path) - let expected_request = request - let actual_request = serveSinglePageApp(request) + let expected_request = request + let actual_request = serveSinglePageApp(request) - t.deepEqual(expected_request, actual_request) + t.deepEqual(expected_request, actual_request) }) diff --git a/src/types.ts b/src/types.ts index db75991..4b78116 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,44 +1,44 @@ export type CacheControl = { - browserTTL: number - edgeTTL: number - bypassCache: boolean + browserTTL: number + edgeTTL: number + bypassCache: boolean } export type AssetManifestType = Record export type Options = { - cacheControl: ((req: Request) => Partial) | Partial - ASSET_NAMESPACE: any - ASSET_MANIFEST: AssetManifestType | string - mapRequestToAsset?: (req: Request, options?: Partial) => Request - defaultMimeType: string - defaultDocument: string - pathIsEncoded: boolean - defaultETag: 'strong' | 'weak' + cacheControl: ((req: Request) => Partial) | Partial + ASSET_NAMESPACE: any + ASSET_MANIFEST: AssetManifestType | string + mapRequestToAsset?: (req: Request, options?: Partial) => Request + defaultMimeType: string + defaultDocument: string + pathIsEncoded: boolean + defaultETag: 'strong' | 'weak' } export class KVError extends Error { - constructor(message?: string, status: number = 500) { - super(message) - // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html - Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain - this.name = KVError.name // stack traces display correctly now - this.status = status - } - status: number + constructor(message?: string, status: number = 500) { + super(message) + // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html + Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain + this.name = KVError.name // stack traces display correctly now + this.status = status + } + status: number } export class MethodNotAllowedError extends KVError { - constructor(message: string = `Not a valid request method`, status: number = 405) { - super(message, status) - } + constructor(message: string = `Not a valid request method`, status: number = 405) { + super(message, status) + } } export class NotFoundError extends KVError { - constructor(message: string = `Not Found`, status: number = 404) { - super(message, status) - } + constructor(message: string = `Not Found`, status: number = 404) { + super(message, status) + } } export class InternalError extends KVError { - constructor(message: string = `Internal Error in KV Asset Handler`, status: number = 500) { - super(message, status) - } + constructor(message: string = `Internal Error in KV Asset Handler`, status: number = 500) { + super(message, status) + } } diff --git a/tsconfig.json b/tsconfig.json index 4a660ef..8bd46ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { - "compilerOptions": { - "outDir": "./dist", - "noImplicitAny": true, - "target": "ES2017", - "allowJs": false, - "lib": ["WebWorker", "ES5", "ScriptHost"], - "module": "commonjs", - "moduleResolution": "node" - }, - "include": ["./src/*.ts", "./src/**/*.ts", "./test/**/*.ts", "./test/*.ts", "./src/types.d.ts"], - "exclude": ["node_modules/", "dist/"] + "compilerOptions": { + "outDir": "./dist", + "noImplicitAny": true, + "target": "ES2017", + "allowJs": false, + "lib": ["WebWorker", "ES5", "ScriptHost"], + "module": "commonjs", + "moduleResolution": "node" + }, + "include": ["./src/*.ts", "./src/**/*.ts", "./test/**/*.ts", "./test/*.ts", "./src/types.d.ts"], + "exclude": ["node_modules/", "dist/"] }