From 2e01544338dd12497c05e3cda8ff5707af5137ca Mon Sep 17 00:00:00 2001 From: Greg Brimble Date: Thu, 18 Jan 2024 18:11:26 -0500 Subject: [PATCH] Prettify kv-asset-handler --- packages/kv-asset-handler/CHANGELOG.md | 45 +- packages/kv-asset-handler/README.md | 66 +- packages/kv-asset-handler/src/index.ts | 299 ++++--- packages/kv-asset-handler/src/mocks.ts | 176 +++-- .../src/test/getAssetFromKV-optional.ts | 35 +- .../src/test/getAssetFromKV.ts | 739 +++++++++--------- .../src/test/mapRequestToAsset.ts | 66 +- .../src/test/serveSinglePageApp.ts | 60 +- packages/kv-asset-handler/src/types.ts | 56 +- packages/kv-asset-handler/tsconfig.json | 8 +- 10 files changed, 825 insertions(+), 725 deletions(-) diff --git a/packages/kv-asset-handler/CHANGELOG.md b/packages/kv-asset-handler/CHANGELOG.md index 65b6fb9399c7..77459b577e70 100644 --- a/packages/kv-asset-handler/CHANGELOG.md +++ b/packages/kv-asset-handler/CHANGELOG.md @@ -34,12 +34,12 @@ - **Allow configurable downgrade of ETag validator strength - [awwong1], [pull/315]** - This allows users to override the default strong ETag validator behaviour to use weak ETag validators. This change allows the developer to use weak ETags and preserve 304 responses (e.g. on *.workers.dev domains). + This allows users to override the default strong ETag validator behaviour to use weak ETag validators. This change allows the developer to use weak ETags and preserve 304 responses (e.g. on \*.workers.dev domains). - ### Fixes - **Fix length property call on ArrayBuffer instance - [philipatkinson], [pull/295**] - + Previously when edge cached was enabled, the `content-length` of the response was not being set correctly. This was due to the `length` property of the `ArrayBuffer` instance being called instead of the `byteLength` property. This PR fixes this issue. - ### Maintenance @@ -51,12 +51,11 @@ - **chore: use tabs for indentation - [Cherry], [pull/355]** This PR changes the indentation of the project to use tabs instead of spaces, falling more in line with other Cloudflare JavaScript projects like wrangler. - + - **chore: bump dependencies - [Cherry], [pull/356]** This bumps many dependencies of the project to their latest versions. - ## 0.2.0 - ### Features @@ -133,15 +132,15 @@ This PR improves performance of the `getAssetFromKV` function by only parsing the asset manifest once on startup, instead of on each request. This can have a significant improvement in response times for larger sites. An example of the performance improvement with an asset manifest of over 50k files: > Before change: - 100 iterations: Done. Mean kv response time is 16.61 - 1000 iterations: Done. Mean kv response time is 17.798 + > 100 iterations: Done. Mean kv response time is 16.61 + > 1000 iterations: Done. Mean kv response time is 17.798 > After change: - 100 iterations: Done. Mean kv response time is 6.62 - 1000 iterations: Done. Mean kv response time is 7.296 + > 100 iterations: Done. Mean kv response time is 6.62 + > 1000 iterations: Done. Mean kv response time is 7.296 Initial work and credit to [groenlid] in [pull/143]. - [Cherry]: https://github.com/Cherry + [cherry]: https://github.com/Cherry [groenlid]: https://github.com/groenlid [pull/185]: https://github.com/cloudflare/kv-asset-handler/pull/185 [pull/143]: https://github.com/cloudflare/kv-asset-handler/pull/143 @@ -165,7 +164,7 @@ This PR tweaks the GitHub Actions Workflow to test PRs properly, both in terms of linting and the repository tests. It runs `prettier` to maintain code quality and style, and all unit tests on every PR to ensure no regressions occur. [pull/183]: https://github.com/cloudflare/kv-asset-handler/pull/185 - [Cherry]: https://github.com/Cherry + [cherry]: https://github.com/Cherry - **Add test for `mapRequestToAsset` asset override - [Cherry], [pull/186]** @@ -173,7 +172,7 @@ [pull/159]: https://github.com/cloudflare/kv-asset-handler/pull/159 [pull/186]: https://github.com/cloudflare/kv-asset-handler/pull/186 - [Cherry]: https://github.com/Cherry + [cherry]: https://github.com/Cherry - **Dependabot updates** @@ -206,9 +205,9 @@ This PR adds support for customizing the `defaultDocument` option in `getAssetFromKV`. In situations where a project does not use `index.html` as the default document for a path, this can now be customized to values like `index.shtm`: ```js - return getAssetFromKV(event, { - defaultDocument: "index.shtm" - }) + return getAssetFromKV(event, { + defaultDocument: "index.shtm", + }); ``` [boemekeld]: https://github.com/boemekeld @@ -222,7 +221,7 @@ **Note that this is a breaking change**, as previously, the mapRequestToAsset function was ignored if you set it, and an exact match was found in the `ASSET_MANIFEST`. That being said, this behavior was a bug, and unexpected behavior, as documented in [issue/158]. - [Cherry]: https://github.com/Cherry + [cherry]: https://github.com/Cherry [issue/158]: https://github.com/cloudflare/kv-asset-handler/pull/158 [pull/159]: https://github.com/cloudflare/kv-asset-handler/pull/159 @@ -279,7 +278,7 @@ New project maintainer Cherry did a ton of maintenance in this release, improving workflows, code quality, and more. Check out the full list in [the PR][pull/179]. - [Cherry]: https://github.com/Cherry + [cherry]: https://github.com/Cherry [pull/179]: https://github.com/cloudflare/kv-asset-handler/pull/179 - ### Documentation @@ -304,10 +303,10 @@ - **kv-asset-handler can translate 206 responses to 200 - [harrishancock], [pull/166]** - Fixes [wrangler#1746](https://github.com/cloudflare/wrangler/issues/1746) + Fixes [wrangler#1746](https://github.com/cloudflare/wrangler/issues/1746) - [harrishancock](https://github.com/harrishancock) - [pull/166](https://github.com/cloudflare/kv-asset-handler/pull/166) + [harrishancock](https://github.com/harrishancock) + [pull/166](https://github.com/cloudflare/kv-asset-handler/pull/166) ## 0.0.12 @@ -328,11 +327,11 @@ - ### Fixes - - **Fix text/* charset - [EatonZ], [pull/130]** + - **Fix text/\* charset - [EatonZ], [pull/130]** Adds a missing `-` to the `utf-8` charset value in response mime types. - [EatonZ]: https://github.com/EatonZ + [eatonz]: https://github.com/EatonZ [pull/130]: https://github.com/cloudflare/kv-asset-handler/pull/130 - **Cache handling for HEAD requests - [klittlepage], [pull/141]** @@ -385,7 +384,7 @@ Fixes an issue where non-ASCII paths were not URI-decoded before being looked up, causing non-ASCII paths to 404. - [SukkaW]: https://github.com/SukkaW + [sukkaw]: https://github.com/SukkaW [pull/105]: https://github.com/cloudflare/kv-asset-handler/pull/105 [issue/99]: https://github.com/cloudflare/kv-asset-handler/issues/99 @@ -415,7 +414,7 @@ - **Add Code of Conduct - [EverlastingBugstopper], [pull/101]** - [EverlastingBugstopper]: https://github.com/EverlastingBugstopper + [everlastingbugstopper]: https://github.com/EverlastingBugstopper [pull/101]: https://github.com/cloudflare/kv-asset-handler/pull/101 ## 0.0.10 diff --git a/packages/kv-asset-handler/README.md b/packages/kv-asset-handler/README.md index bdfdb85579a5..d32029110b9a 100644 --- a/packages/kv-asset-handler/README.md +++ b/packages/kv-asset-handler/README.md @@ -71,63 +71,71 @@ Known errors to be thrown are: #### ES Modules ```js -import { getAssetFromKV, NotFoundError, MethodNotAllowedError } from '@cloudflare/kv-asset-handler' -import manifestJSON from '__STATIC_CONTENT_MANIFEST' -const assetManifest = JSON.parse(manifestJSON) +import { + getAssetFromKV, + NotFoundError, + MethodNotAllowedError, +} from "@cloudflare/kv-asset-handler"; +import manifestJSON from "__STATIC_CONTENT_MANIFEST"; +const assetManifest = JSON.parse(manifestJSON); export default { async fetch(request, env, ctx) { - if (request.url.includes('/docs')) { + if (request.url.includes("/docs")) { try { return await getAssetFromKV( { request, waitUntil(promise) { - return ctx.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 }) + return new Response("An unexpected error occurred", { status: 500 }); } } - } else return fetch(request) + } else return fetch(request); }, -} +}; ``` #### Service Worker ```js -import { getAssetFromKV, NotFoundError, MethodNotAllowedError } from '@cloudflare/kv-asset-handler' +import { + getAssetFromKV, + NotFoundError, + MethodNotAllowedError, +} from "@cloudflare/kv-asset-handler"; -addEventListener('fetch', (event) => { - event.respondWith(handleEvent(event)) -}) +addEventListener("fetch", (event) => { + event.respondWith(handleEvent(event)); +}); async function handleEvent(event) { - if (event.request.url.includes('/docs')) { + if (event.request.url.includes("/docs")) { try { - return await getAssetFromKV(event) + 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 }) + return new Response("An unexpected error occurred", { status: 500 }); } } - } else return fetch(event.request) + } else return fetch(event.request); } ``` @@ -176,7 +184,7 @@ 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` @@ -216,13 +224,13 @@ return getAssetFromKV( { request, waitUntil(promise) { - return ctx.waitUntil(promise) + return ctx.waitUntil(promise); }, }, { ASSET_NAMESPACE: env.__STATIC_CONTENT, - }, -) + } +); ``` ##### Service Worker @@ -244,22 +252,22 @@ In ES Modules format, this argument is required, and can be imported. ##### ES Module ```js -import manifestJSON from '__STATIC_CONTENT_MANIFEST' -let manifest = JSON.parse(manifestJSON) -manifest['index.html'] = 'index.special.html' +import manifestJSON from "__STATIC_CONTENT_MANIFEST"; +let manifest = JSON.parse(manifestJSON); +manifest["index.html"] = "index.special.html"; return getAssetFromKV( { request, waitUntil(promise) { - return ctx.waitUntil(promise) + return ctx.waitUntil(promise); }, }, { ASSET_MANIFEST: manifest, // ... - }, -) + } +); ``` ##### Service Worker @@ -329,7 +337,7 @@ To turn `etags` **off**, you must bypass cache: /* Turn etags off */ let cacheControl = { bypassCache: true, -} +}; ``` #### Syntax and comparison context diff --git a/packages/kv-asset-handler/src/index.ts b/packages/kv-asset-handler/src/index.ts index 4a745a43662a..0953ed0a1fa7 100644 --- a/packages/kv-asset-handler/src/index.ts +++ b/packages/kv-asset-handler/src/index.ts @@ -1,4 +1,4 @@ -import * as mime from 'mime' +import * as mime from "mime"; import { Options, CacheControl, @@ -6,38 +6,41 @@ import { NotFoundError, InternalError, AssetManifestType, -} from './types' +} 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 -} +}; 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_NAMESPACE: + typeof __STATIC_CONTENT !== "undefined" ? __STATIC_CONTENT : undefined, ASSET_MANIFEST: - typeof __STATIC_CONTENT_MANIFEST !== 'undefined' + typeof __STATIC_CONTENT_MANIFEST !== "undefined" ? parseStringAsObject(__STATIC_CONTENT_MANIFEST) : {}, cacheControl: defaultCacheControl, - defaultMimeType: 'text/plain', - defaultDocument: 'index.html', + defaultMimeType: "text/plain", + defaultDocument: "index.html", pathIsEncoded: false, - defaultETag: 'strong', -} + 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) + return Object.assign({}, getAssetFromKVDefaultOptions, options); } /** @@ -48,48 +51,54 @@ function assignOptions(options?: Partial): Options { * @param {Request} request incoming request */ const mapRequestToAsset = (request: Request, options?: Partial) => { - options = assignOptions(options) + options = assignOptions(options); - const parsedUrl = new URL(request.url) - let pathname = parsedUrl.pathname + const parsedUrl = new URL(request.url); + let pathname = parsedUrl.pathname; - if (pathname.endsWith('/')) { + 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) + 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) + pathname = pathname.concat("/" + options.defaultDocument); } - parsedUrl.pathname = pathname - return new Request(parsedUrl.toString(), request) -} + parsedUrl.pathname = pathname; + return new Request(parsedUrl.toString(), request); +}; /** * maps the path of incoming request to /index.html if it evaluates to * any HTML file. * @param {Request} request incoming request */ -function serveSinglePageApp(request: Request, options?: Partial): Request { - options = assignOptions(options) +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) + request = mapRequestToAsset(request, options); - const parsedUrl = new URL(request.url) + 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 (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) + 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 + return request; } } @@ -106,211 +115,241 @@ function serveSinglePageApp(request: Request, options?: Partial): Reque * */ type Evt = { - 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`) + 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 + 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) + requestKey = options.mapRequestToAsset(request); } else if (ASSET_MANIFEST[rawPathKey]) { - requestKey = request + requestKey = request; } else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) { - pathIsEncoded = true - requestKey = request + pathIsEncoded = true; + requestKey = request; } else { - const mappedRequest = mapRequestToAsset(request) - const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, '') + const mappedRequest = mapRequestToAsset(request); + const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace( + /^\/+/, + "" + ); if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) { - pathIsEncoded = true - requestKey = mappedRequest + pathIsEncoded = true; + requestKey = mappedRequest; } else { // use default mapRequestToAsset - requestKey = mapRequestToAsset(request, options) + requestKey = mapRequestToAsset(request, options); } } - const SUPPORTED_METHODS = ['GET', 'HEAD'] + const SUPPORTED_METHODS = ["GET", "HEAD"]; if (!SUPPORTED_METHODS.includes(requestKey.method)) { - throw new MethodNotAllowedError(`${requestKey.method} is not a valid request 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 + 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 / + 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' + 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 + 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 (typeof ASSET_MANIFEST !== "undefined") { if (ASSET_MANIFEST[pathKey]) { - pathKey = 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 + shouldEdgeCache = true; } } // TODO this excludes search params from cache, investigate ideal behavior - let cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request) + 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 + case "function": + return options.cacheControl(request); + case "object": + return options.cacheControl; default: - return defaultCacheControl + 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) => { + const formatETag = ( + entityId: any = pathKey, + validatorType: string = options.defaultETag + ) => { if (!entityId) { - return '' + return ""; } switch (validatorType) { - case 'weak': - if (!entityId.startsWith('W/')) { + case "weak": + if (!entityId.startsWith("W/")) { if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) { - return `W/${entityId}` + return `W/${entityId}`; } - return `W/"${entityId}"` + return `W/"${entityId}"`; } - return entityId - case 'strong': + return entityId; + case "strong": if (entityId.startsWith(`W/"`)) { - entityId = entityId.replace('W/', '') + entityId = entityId.replace("W/", ""); } if (!entityId.endsWith(`"`)) { - entityId = `"${entityId}"` + entityId = `"${entityId}"`; } - return entityId + return entityId; default: - return '' + return ""; } - } + }; - options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts) + options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts); // override shouldEdgeCache if options say to bypassCache if ( options.cacheControl.bypassCache || options.cacheControl.edgeTTL === null || - request.method == 'HEAD' + request.method == "HEAD" ) { - shouldEdgeCache = false + shouldEdgeCache = false; } // only set max-age if explicitly passed in a number as an arg - const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number' + const shouldSetBrowserCache = + typeof options.cacheControl.browserTTL === "number"; - let response = null + let response = null; if (shouldEdgeCache) { - response = await cache.match(cacheKey) + response = await cache.match(cacheKey); } if (response) { if (response.status > 300 && response.status < 400) { - if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) { + if (response.body && "cancel" in Object.getPrototypeOf(response.body)) { // Body exists and environment supports readable streams - response.body.cancel() + response.body.cancel(); } else { // Environment doesnt support readable streams, or null repsonse body. Nothing to do } - response = new Response(null, response) + response = new Response(null, response); } else { // fixes #165 let opts = { headers: new Headers(response.headers), status: 0, - statusText: '', - } + statusText: "", + }; - opts.headers.set('cf-cache-status', 'HIT') + 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' + 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' + opts.status = 200; + opts.statusText = "OK"; } - response = new Response(response.body, opts) + response = new Response(response.body, opts); } } else { - const body = await ASSET_NAMESPACE.get(pathKey, 'arrayBuffer') + const body = await ASSET_NAMESPACE.get(pathKey, "arrayBuffer"); if (body === null) { - throw new NotFoundError(`could not find ${pathKey} in your content namespace`) + throw new NotFoundError( + `could not find ${pathKey} in your content namespace` + ); } - response = new Response(body) + response = new Response(body); if (shouldEdgeCache) { - response.headers.set('Accept-Ranges', 'bytes') - response.headers.set('Content-Length', String(body.byteLength)) + 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)) + 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( + "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) + 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') + 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') + if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === "MISS") { + response.headers.set("CF-Cache-Status", "EXPIRED"); } else { - response.headers.set('CF-Cache-Status', 'REVALIDATED') + response.headers.set("CF-Cache-Status", "REVALIDATED"); } - response.headers.set('etag', formatETag(etag, 'weak')) + response.headers.set("etag", formatETag(etag, "weak")); } } if (shouldSetBrowserCache) { - response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`) + response.headers.set( + "Cache-Control", + `max-age=${options.cacheControl.browserTTL}` + ); } else { - response.headers.delete('Cache-Control') + response.headers.delete("Cache-Control"); } - return response -} + return response; +}; -export { getAssetFromKV, mapRequestToAsset, serveSinglePageApp } -export { Options, CacheControl, MethodNotAllowedError, NotFoundError, InternalError } +export { getAssetFromKV, mapRequestToAsset, serveSinglePageApp }; +export { + Options, + CacheControl, + MethodNotAllowedError, + NotFoundError, + InternalError, +}; diff --git a/packages/kv-asset-handler/src/mocks.ts b/packages/kv-asset-handler/src/mocks.ts index ad0830df99d3..f3943a90b056 100644 --- a/packages/kv-asset-handler/src/mocks.ts +++ b/packages/kv-asset-handler/src/mocks.ts @@ -1,66 +1,66 @@ -const makeServiceWorkerEnv = require('service-worker-mock') +const makeServiceWorkerEnv = require("service-worker-mock"); -const HASH = '123HASHBROWN' +const HASH = "123HASHBROWN"; export const getEvent = (request: Request): any => { const waitUntil = async (callback: any) => { - await callback - } + 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, - } -} + }; +}; 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`, + "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`, - }) -} + "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() +let cacheStore: any = new Map(); interface CacheKey { - url: object - headers: object + url: object; + headers: object; } export const mockCaches = () => { return { @@ -69,80 +69,84 @@ export const mockCaches = () => { 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)) + }; + 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()) + const activeCacheKeys: Array = Array.from(cacheStore.keys()); for (const cacheStoreKey of activeCacheKeys) { if (JSON.parse(cacheStoreKey).url === key.url) { - response = cacheStore.get(cacheStoreKey) + 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')) { + 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') + const range = key.headers.get("range"); if (range) { response.headers.set( - 'content-range', - `bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`, - ) + "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 + if (response.headers.has("content-range")) { + response.status = 206; } else { - response.status = 200 + response.status = 200; } - let etag = response.headers.get('etag') - if (etag && !etag.includes('W/')) { - response.headers.set('etag', `W/${etag}`) + let etag = response.headers.get("etag"); + if (etag && !etag.includes("W/")) { + response.headers.set("etag", `W/${etag}`); } } - return response + 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 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('/', '')}"`, + etag: `"${url.pathname.replace("/", "")}"`, }, - } - cacheStore.set(JSON.stringify(cacheKey), resNoBody) - cacheKey.headers = {} - cacheStore.set(JSON.stringify(cacheKey), resWithBody) - return + }; + 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/packages/kv-asset-handler/src/test/getAssetFromKV-optional.ts b/packages/kv-asset-handler/src/test/getAssetFromKV-optional.ts index afe956ad985c..f414e789c2aa 100644 --- a/packages/kv-asset-handler/src/test/getAssetFromKV-optional.ts +++ b/packages/kv-asset-handler/src/test/getAssetFromKV-optional.ts @@ -1,24 +1,31 @@ -import test from 'ava' -import { mockRequestScope, mockGlobalScope, getEvent, sleep, mockKV, mockManifest } from '../mocks' -mockGlobalScope() +import test from "ava"; +import { + mockRequestScope, + mockGlobalScope, + getEvent, + sleep, + mockKV, + mockManifest, +} from "../mocks"; +mockGlobalScope(); // manually reset manifest global, to test optional behaviour -Object.assign(global, { __STATIC_CONTENT_MANIFEST: undefined }) +Object.assign(global, { __STATIC_CONTENT_MANIFEST: undefined }); -import { getAssetFromKV, mapRequestToAsset } from '../index' +import { getAssetFromKV, mapRequestToAsset } from "../index"; -test('getAssetFromKV return correct val from KV without manifest', async (t) => { - mockRequestScope() +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 }) + 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')) + t.is(await res.text(), "val1"); + t.true(res.headers.get("content-type").includes("text")); } else { - t.fail('Response was undefined') + t.fail("Response was undefined"); } -}) +}); diff --git a/packages/kv-asset-handler/src/test/getAssetFromKV.ts b/packages/kv-asset-handler/src/test/getAssetFromKV.ts index 0844a00517bf..8bdfdabded0a 100644 --- a/packages/kv-asset-handler/src/test/getAssetFromKV.ts +++ b/packages/kv-asset-handler/src/test/getAssetFromKV.ts @@ -1,524 +1,551 @@ -import test from 'ava' -import { mockRequestScope, mockGlobalScope, getEvent, sleep, mockKV, mockManifest } from '../mocks' -mockGlobalScope() - -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) +import test from "ava"; +import { + mockRequestScope, + mockGlobalScope, + getEvent, + sleep, + mockKV, + mockManifest, +} from "../mocks"; +mockGlobalScope(); + +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')) + 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') + 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')) -}) -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')) -}) - -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) +}); +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")); +}); +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")); +}); + +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); if (res) { - t.is(await res.text(), 'no hash but still got some result') - t.true(res.headers.get('content-type').includes('text')) + 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') + 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) -}) - -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) +}); + +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); +}); + +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') + t.is(await res.text(), "picturedis"); } else { - t.fail('Response was undefined') + 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) +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); if (res) { - t.is(await res.text(), 'index.html') - t.true(res.headers.get('content-type').includes('html')) + t.is(await res.text(), "index.html"); + t.true(res.headers.get("content-type").includes("html")); } else { - t.fail('Response was undefined') + 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) +test("getAssetFromKV non ASCII path support", async (t) => { + 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') + t.is(await res.text(), "My filename is non-ascii"); } else { - t.fail('Response was undefined') + 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) +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); if (res) { - t.is(await res.text(), 'browser percent encoded') + t.is(await res.text(), "browser percent encoded"); } else { - t.fail('Response was undefined') + 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) +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); if (res) { - t.is(await res.text(), 'user percent encoded') + t.is(await res.text(), "user percent encoded"); } else { - t.fail('Response was undefined') + 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) +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); if (res1 && res2) { - t.is(await res1.text(), 'Im important') - t.is(await res2.text(), 'Im important') + t.is(await res1.text(), "Im important"); + t.is(await res2.text(), "Im important"); } else { - t.fail('Response was undefined') + 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) +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); if (res1 && res2) { - t.is(await res1.text(), 'My path is non-ascii') - t.is(await res2.text(), 'My path is non-ascii') + 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') + 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')) +test("getAssetFromKV custom key modifier", async (t) => { + mockRequestScope(); + const event = getEvent(new Request("https://blah.com/docs/sub/blah.png")); const customRequestMapper = (request: Request) => { - let defaultModifiedRequest = mapRequestToAsset(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') + t.is(await res.text(), "picturedis"); } else { - t.fail('Response was undefined') + t.fail("Response was undefined"); } -}) +}); -test('getAssetFromKV request override with existing manifest file', async (t) => { +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 + mockRequestScope(); + const event = getEvent(new Request("https://blah.com/image.png")); // real file in manifest const customRequestMapper = (request: Request) => { - let defaultModifiedRequest = mapRequestToAsset(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') + t.is(await res.text(), "imagewebp"); } else { - t.fail('Response was undefined') + t.fail("Response was undefined"); } -}) +}); -test('getAssetFromKV when setting browser caching', async (t) => { - mockRequestScope() - const event = getEvent(new Request('https://blah.com/')) +test("getAssetFromKV when setting browser caching", async (t) => { + 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') + t.is(res.headers.get("cache-control"), "max-age=22"); } else { - t.fail('Response was undefined') + 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')) +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')) + 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 }) + 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') + 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') + 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}`)) +}); +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}"`, + "if-none-match": `"${resourceVersion}"`, }, - }), - ) + }) + ); - const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720, browserTTL: 720 } }) - await sleep(1) - const res2 = await getAssetFromKV(event2) + 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') + 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') + 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}`)) +}); +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}"`, + "if-none-match": `"${resourceVersion}"`, }, - }), - ) + }) + ); - const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) - await sleep(100) - const res2 = await getAssetFromKV(event2) + 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) + 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') + 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/')) +test("getAssetFromKV does not cache on Cloudflare when bypass cache set", async (t) => { + 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) + t.is(res.headers.get("cache-control"), null); + t.is(res.headers.get("cf-cache-status"), null); } else { - t.fail('Response was undefined') + 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) +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') + t.is(await res.text(), "index.html"); } else { - t.fail('Response was undefined') + 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) +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') + t.is(await res.text(), "picturedis"); } else { - t.fail('Response was undefined') + 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) -}) -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 } }) +}); + +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); +}); +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) + 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') + t.fail("Response was undefined"); } -}) -test('getAssetFromKV passing in a custom NAMESPACE serves correct asset', async (t) => { - mockRequestScope() +}); +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) + "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')) + t.is(await res.text(), "index.html"); + t.true(res.headers.get("content-type").includes("html")); } else { - t.fail('Response was undefined') + t.fail("Response was undefined"); } -}) -test('getAssetFromKV when custom namespace without the asset should fail', async (t) => { - mockRequestScope() +}); +test("getAssetFromKV when custom namespace without the asset should fail", async (t) => { + mockRequestScope(); let CUSTOM_NAMESPACE = mockKV({ - 'key5.123HASHBROWN.txt': 'customvalu', - }) + "key5.123HASHBROWN.txt": "customvalu", + }); - const event = getEvent(new Request('https://blah.com')) + 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 }) - - const event = getEvent(new Request('https://blah.com/')) + 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 }); + + 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}`)) + 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}"`, + "if-none-match": `W/"${resourceVersion}"`, }, - }), - ) + }) + ); - const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } }) - await sleep(100) - const res2 = await getAssetFromKV(event2) + 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') + t.is(res1.headers.get("cf-cache-status"), "MISS"); + t.is(res2.headers.get("cf-cache-status"), "REVALIDATED"); } else { - t.fail('Response was undefined') + 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] +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}"`, + "if-none-match": `"${resourceVersion}"`, }, - }) + }); const req2 = new Request(`https://blah.com/${resourceKey}`, { headers: { - 'if-none-match': `"${resourceVersion}-another-version"`, + "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) + }); + 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') + 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') + 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] +}); +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}"`, + "if-none-match": `"${resourceVersion}"`, }, - }) - const event = getEvent(req1) - const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } }) - const res2 = await getAssetFromKV(event) + }); + 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')}`) + 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') + 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] +}); +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}"`, + "if-none-match": `W/"${resourceVersion}"`, }, - }) + }); const req2 = new Request(`https://blah-weak.com/${resourceKey}`, { headers: { - 'if-none-match': `"${resourceVersion}"`, + "if-none-match": `"${resourceVersion}"`, }, - }) + }); const req3 = new Request(`https://blah-weak.com/${resourceKey}`, { headers: { - 'if-none-match': `"${resourceVersion}-another-version"`, + "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' }) + }); + 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')) + 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') + 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) +}); + +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') + 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') + 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}`)) +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) + 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') + t.fail("Response was undefined"); } -}) +}); -test.todo('getAssetFromKV when body not empty, should invoke .cancel()') +test.todo("getAssetFromKV when body not empty, should invoke .cancel()"); diff --git a/packages/kv-asset-handler/src/test/mapRequestToAsset.ts b/packages/kv-asset-handler/src/test/mapRequestToAsset.ts index 94e37fe13d35..3672bfbec7d4 100644 --- a/packages/kv-asset-handler/src/test/mapRequestToAsset.ts +++ b/packages/kv-asset-handler/src/test/mapRequestToAsset.ts @@ -1,37 +1,39 @@ -import test from 'ava' -import { mockRequestScope, mockGlobalScope } from '../mocks' -mockGlobalScope() +import test from "ava"; +import { mockRequestScope, mockGlobalScope } from "../mocks"; +mockGlobalScope(); -import { mapRequestToAsset } from '../index' +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') -}) +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"); +}); -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') -}) +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"); +}); -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') -}) +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"); +}); -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') -}) +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"); +}); diff --git a/packages/kv-asset-handler/src/test/serveSinglePageApp.ts b/packages/kv-asset-handler/src/test/serveSinglePageApp.ts index b299fcfb6307..04cd23639f22 100644 --- a/packages/kv-asset-handler/src/test/serveSinglePageApp.ts +++ b/packages/kv-asset-handler/src/test/serveSinglePageApp.ts @@ -1,44 +1,44 @@ -import test from 'ava' -import { mockRequestScope, mockGlobalScope } from '../mocks' -mockGlobalScope() +import test from "ava"; +import { mockRequestScope, mockGlobalScope } from "../mocks"; +mockGlobalScope(); -import { serveSinglePageApp } from '../index' +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) +test("serveSinglePageApp returns root asset path when request path ends in .html", async (t) => { + 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) +test("serveSinglePageApp returns root asset path when request path does not have extension", async (t) => { + 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) +test("serveSinglePageApp returns requested asset when request path has non-html extension", async (t) => { + 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/packages/kv-asset-handler/src/types.ts b/packages/kv-asset-handler/src/types.ts index 4b7811640c93..03ea856a3669 100644 --- a/packages/kv-asset-handler/src/types.ts +++ b/packages/kv-asset-handler/src/types.ts @@ -1,44 +1,52 @@ export type CacheControl = { - browserTTL: number - edgeTTL: number - bypassCache: boolean -} + browserTTL: number; + edgeTTL: number; + bypassCache: boolean; +}; -export type AssetManifestType = Record +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) + 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 + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + this.name = KVError.name; // stack traces display correctly now + this.status = status; } - status: number + 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) + 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/packages/kv-asset-handler/tsconfig.json b/packages/kv-asset-handler/tsconfig.json index 8bd46ac7f3a2..fbac2f58bf28 100644 --- a/packages/kv-asset-handler/tsconfig.json +++ b/packages/kv-asset-handler/tsconfig.json @@ -8,6 +8,12 @@ "module": "commonjs", "moduleResolution": "node" }, - "include": ["./src/*.ts", "./src/**/*.ts", "./test/**/*.ts", "./test/*.ts", "./src/types.d.ts"], + "include": [ + "./src/*.ts", + "./src/**/*.ts", + "./test/**/*.ts", + "./test/*.ts", + "./src/types.d.ts" + ], "exclude": ["node_modules/", "dist/"] }