309 lines
13 KiB
JavaScript
309 lines
13 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.InternalError = exports.NotFoundError = exports.MethodNotAllowedError = exports.mapRequestToAsset = exports.getAssetFromKV = void 0;
|
|
exports.serveSinglePageApp = serveSinglePageApp;
|
|
const mime = __importStar(require("mime"));
|
|
const types_1 = require("./types");
|
|
Object.defineProperty(exports, "InternalError", { enumerable: true, get: function () { return types_1.InternalError; } });
|
|
Object.defineProperty(exports, "MethodNotAllowedError", { enumerable: true, get: function () { return types_1.MethodNotAllowedError; } });
|
|
Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return types_1.NotFoundError; } });
|
|
const defaultCacheControl = {
|
|
browserTTL: null,
|
|
edgeTTL: 2 * 60 * 60 * 24, // 2 days
|
|
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,
|
|
defaultETag: "strong",
|
|
};
|
|
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;
|
|
}
|
|
}
|
|
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-expect-error we should pick cf types here
|
|
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
|
|
const 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 = 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
|
|
const 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", 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) {
|
|
const etag = formatETag(response.headers.get("etag"));
|
|
const ifNoneMatch = cacheKey.headers.get("if-none-match");
|
|
const 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;
|