diff options
| author | akiyamn | 2023-09-24 23:22:21 +1000 |
|---|---|---|
| committer | akiyamn | 2023-09-24 23:22:21 +1000 |
| commit | 4e87195739f2a5d9a05451b48773c8afdc680765 (patch) | |
| tree | 9cba501844a4a11dcbdffc4050ed8189561c55ed /node_modules/@cloudflare/kv-asset-handler/dist/index.js | |
| download | price-tracker-worker-4e87195739f2a5d9a05451b48773c8afdc680765.tar.gz price-tracker-worker-4e87195739f2a5d9a05451b48773c8afdc680765.zip | |
Initial commit (by create-cloudflare CLI)
Diffstat (limited to 'node_modules/@cloudflare/kv-asset-handler/dist/index.js')
| -rw-r--r-- | node_modules/@cloudflare/kv-asset-handler/dist/index.js | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/node_modules/@cloudflare/kv-asset-handler/dist/index.js b/node_modules/@cloudflare/kv-asset-handler/dist/index.js new file mode 100644 index 0000000..5f80b5e --- /dev/null +++ b/node_modules/@cloudflare/kv-asset-handler/dist/index.js @@ -0,0 +1,267 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.InternalError = exports.NotFoundError = exports.MethodNotAllowedError = exports.serveSinglePageApp = exports.mapRequestToAsset = exports.getAssetFromKV = void 0; +const mime = require("mime"); +const types_1 = require("./types"); +Object.defineProperty(exports, "MethodNotAllowedError", { enumerable: true, get: function () { return types_1.MethodNotAllowedError; } }); +Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return types_1.NotFoundError; } }); +Object.defineProperty(exports, "InternalError", { enumerable: true, get: function () { return types_1.InternalError; } }); +const defaultCacheControl = { + browserTTL: null, + edgeTTL: 2 * 60 * 60 * 24, + bypassCache: false, // do not bypass Cloudflare's cache +}; +const parseStringAsObject = (maybeString) => typeof maybeString === 'string' ? JSON.parse(maybeString) : maybeString; +const getAssetFromKVDefaultOptions = { + 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, +}; +function assignOptions(options) { + // Assign any missing options passed in to the default + // options.mapRequestToAsset is handled manually later + return Object.assign({}, getAssetFromKVDefaultOptions, options); +} +/** + * maps the path of incoming request to the request pathKey to look up + * in bucket and in cache + * e.g. for a path '/' returns '/index.html' which serves + * the content of bucket/index.html + * @param {Request} request incoming request + */ +const mapRequestToAsset = (request, options) => { + 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); +}; +exports.mapRequestToAsset = mapRequestToAsset; +/** + * maps the path of incoming request to /index.html if it evaluates to + * any HTML file. + * @param {Request} request incoming request + */ +function serveSinglePageApp(request, options) { + 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; + } +} +exports.serveSinglePageApp = serveSinglePageApp; +const getAssetFromKV = async (event, options) => { + 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 types_1.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 types_1.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 = pathKey, validatorType = 'strong') => { + if (!entityId) { + return ''; + } + switch (validatorType) { + case 'weak': + if (!entityId.startsWith('W/')) { + 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 types_1.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', body.length); + // set etag before cache insertion + if (!response.headers.has('etag')) { + response.headers.set('etag', formatETag(pathKey, 'strong')); + } + // 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'), 'strong'); + 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; +}; +exports.getAssetFromKV = getAssetFromKV; |
