UNPKG

13.9 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11var __generator = (this && this.__generator) || function (thisArg, body) {
12 var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13 return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14 function verb(n) { return function (v) { return step([n, v]); }; }
15 function step(op) {
16 if (f) throw new TypeError("Generator is already executing.");
17 while (_) try {
18 if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19 if (y = 0, t) op = [op[0] & 2, t.value];
20 switch (op[0]) {
21 case 0: case 1: t = op; break;
22 case 4: _.label++; return { value: op[1], done: false };
23 case 5: _.label++; y = op[1]; op = [0]; continue;
24 case 7: op = _.ops.pop(); _.trys.pop(); continue;
25 default:
26 if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27 if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28 if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29 if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30 if (t[2]) _.ops.pop();
31 _.trys.pop(); continue;
32 }
33 op = body.call(thisArg, _);
34 } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35 if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36 }
37};
38Object.defineProperty(exports, "__esModule", { value: true });
39exports.InternalError = exports.NotFoundError = exports.MethodNotAllowedError = exports.serveSinglePageApp = exports.mapRequestToAsset = exports.getAssetFromKV = void 0;
40var mime = require("mime");
41var types_1 = require("./types");
42Object.defineProperty(exports, "MethodNotAllowedError", { enumerable: true, get: function () { return types_1.MethodNotAllowedError; } });
43Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return types_1.NotFoundError; } });
44Object.defineProperty(exports, "InternalError", { enumerable: true, get: function () { return types_1.InternalError; } });
45/**
46 * maps the path of incoming request to the request pathKey to look up
47 * in bucket and in cache
48 * e.g. for a path '/' returns '/index.html' which serves
49 * the content of bucket/index.html
50 * @param {Request} request incoming request
51 */
52var mapRequestToAsset = function (request) {
53 var parsedUrl = new URL(request.url);
54 var pathname = parsedUrl.pathname;
55 if (pathname.endsWith('/')) {
56 // If path looks like a directory append index.html
57 // e.g. If path is /about/ -> /about/index.html
58 pathname = pathname.concat('index.html');
59 }
60 else if (!mime.getType(pathname)) {
61 // If path doesn't look like valid content
62 // e.g. /about.me -> /about.me/index.html
63 pathname = pathname.concat('/index.html');
64 }
65 parsedUrl.pathname = pathname;
66 return new Request(parsedUrl.toString(), request);
67};
68exports.mapRequestToAsset = mapRequestToAsset;
69/**
70 * maps the path of incoming request to /index.html if it evaluates to
71 * any HTML file.
72 * @param {Request} request incoming request
73 */
74function serveSinglePageApp(request) {
75 // First apply the default handler, which already has logic to detect
76 // paths that should map to HTML files.
77 request = mapRequestToAsset(request);
78 var parsedUrl = new URL(request.url);
79 // Detect if the default handler decided to map to
80 // a HTML file in some specific directory.
81 if (parsedUrl.pathname.endsWith('.html')) {
82 // If expected HTML file was missing, just return the root index.html
83 return new Request(parsedUrl.origin + "/index.html", request);
84 }
85 else {
86 // The default handler decided this is not an HTML page. It's probably
87 // an image, CSS, or JS file. Leave it as-is.
88 return request;
89 }
90}
91exports.serveSinglePageApp = serveSinglePageApp;
92var defaultCacheControl = {
93 browserTTL: null,
94 edgeTTL: 2 * 60 * 60 * 24,
95 bypassCache: false,
96};
97/**
98 * takes the path of the incoming request, gathers the appropriate content from KV, and returns
99 * the response
100 *
101 * @param {FetchEvent} event the fetch event of the triggered request
102 * @param {{mapRequestToAsset: (string: Request) => Request, cacheControl: {bypassCache:boolean, edgeTTL: number, browserTTL:number}, ASSET_NAMESPACE: any, ASSET_MANIFEST:any}} [options] configurable options
103 * @param {CacheControl} [options.cacheControl] determine how to cache on Cloudflare and the browser
104 * @param {typeof(options.mapRequestToAsset)} [options.mapRequestToAsset] maps the path of incoming request to the request pathKey to look up
105 * @param {Object | string} [options.ASSET_NAMESPACE] the binding to the namespace that script references
106 * @param {any} [options.ASSET_MANIFEST] the map of the key to cache and store in KV
107 * */
108var getAssetFromKV = function (event, options) { return __awaiter(void 0, void 0, void 0, function () {
109 var request, ASSET_NAMESPACE, ASSET_MANIFEST, SUPPORTED_METHODS, rawPathKey, pathIsEncoded, requestKey, parsedUrl, pathname, pathKey, cache, mimeType, shouldEdgeCache, cacheKey, evalCacheOpts, shouldSetBrowserCache, response, headers, shouldRevalidate, body;
110 return __generator(this, function (_a) {
111 switch (_a.label) {
112 case 0:
113 // Assign any missing options passed in to the default
114 options = Object.assign({
115 ASSET_NAMESPACE: __STATIC_CONTENT,
116 ASSET_MANIFEST: __STATIC_CONTENT_MANIFEST,
117 mapRequestToAsset: mapRequestToAsset,
118 cacheControl: defaultCacheControl,
119 defaultMimeType: 'text/plain',
120 }, options);
121 request = event.request;
122 ASSET_NAMESPACE = options.ASSET_NAMESPACE;
123 ASSET_MANIFEST = typeof (options.ASSET_MANIFEST) === 'string'
124 ? JSON.parse(options.ASSET_MANIFEST)
125 : options.ASSET_MANIFEST;
126 if (typeof ASSET_NAMESPACE === 'undefined') {
127 throw new types_1.InternalError("there is no KV namespace bound to the script");
128 }
129 SUPPORTED_METHODS = ['GET', 'HEAD'];
130 if (!SUPPORTED_METHODS.includes(request.method)) {
131 throw new types_1.MethodNotAllowedError(request.method + " is not a valid request method");
132 }
133 rawPathKey = new URL(request.url).pathname.replace(/^\/+/, '') // strip any preceding /'s
134 ;
135 pathIsEncoded = false;
136 if (ASSET_MANIFEST[rawPathKey]) {
137 requestKey = request;
138 }
139 else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) {
140 pathIsEncoded = true;
141 requestKey = request;
142 }
143 else {
144 requestKey = options.mapRequestToAsset(request);
145 }
146 parsedUrl = new URL(requestKey.url);
147 pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary
148 ;
149 pathKey = pathname.replace(/^\/+/, '') // remove prepended /
150 ;
151 cache = caches.default;
152 mimeType = mime.getType(pathKey) || options.defaultMimeType;
153 if (mimeType.startsWith('text')) {
154 mimeType += '; charset=utf-8';
155 }
156 shouldEdgeCache = false // false if storing in KV by raw file path i.e. no hash
157 ;
158 // check manifest for map from file path to hash
159 if (typeof ASSET_MANIFEST !== 'undefined') {
160 if (ASSET_MANIFEST[pathKey]) {
161 pathKey = ASSET_MANIFEST[pathKey];
162 // if path key is in asset manifest, we can assume it contains a content hash and can be cached
163 shouldEdgeCache = true;
164 }
165 }
166 cacheKey = new Request(parsedUrl.origin + "/" + pathKey, request);
167 evalCacheOpts = (function () {
168 switch (typeof options.cacheControl) {
169 case 'function':
170 return options.cacheControl(request);
171 case 'object':
172 return options.cacheControl;
173 default:
174 return defaultCacheControl;
175 }
176 })();
177 options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts);
178 // override shouldEdgeCache if options say to bypassCache
179 if (options.cacheControl.bypassCache ||
180 options.cacheControl.edgeTTL === null ||
181 request.method == 'HEAD') {
182 shouldEdgeCache = false;
183 }
184 shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number';
185 response = null;
186 if (!shouldEdgeCache) return [3 /*break*/, 2];
187 return [4 /*yield*/, cache.match(cacheKey)];
188 case 1:
189 response = _a.sent();
190 _a.label = 2;
191 case 2:
192 if (!response) return [3 /*break*/, 3];
193 headers = new Headers(response.headers);
194 shouldRevalidate = false;
195 // Four preconditions must be met for a 304 Not Modified:
196 // - the request cannot be a range request
197 // - client sends if-none-match
198 // - resource has etag
199 // - test if-none-match against the pathKey so that we test against KV, rather than against
200 // CF cache, which may modify the etag with a weak validator (e.g. W/"...")
201 shouldRevalidate = [
202 request.headers.has('range') !== true,
203 request.headers.has('if-none-match'),
204 response.headers.has('etag'),
205 request.headers.get('if-none-match') === "" + pathKey,
206 ].every(Boolean);
207 if (shouldRevalidate) {
208 // fixes issue #118
209 if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) {
210 response.body.cancel();
211 console.log('Body exists and environment supports readable streams. Body cancelled');
212 }
213 else {
214 console.log('Environment doesnt support readable streams');
215 }
216 headers.set('cf-cache-status', 'REVALIDATED');
217 response = new Response(null, {
218 status: 304,
219 headers: headers,
220 statusText: 'Not Modified',
221 });
222 }
223 else {
224 headers.set('CF-Cache-Status', 'HIT');
225 response = new Response(response.body, { headers: headers });
226 }
227 return [3 /*break*/, 5];
228 case 3: return [4 /*yield*/, ASSET_NAMESPACE.get(pathKey, 'arrayBuffer')];
229 case 4:
230 body = _a.sent();
231 if (body === null) {
232 throw new types_1.NotFoundError("could not find " + pathKey + " in your content namespace");
233 }
234 response = new Response(body);
235 if (shouldEdgeCache) {
236 response.headers.set('Accept-Ranges', 'bytes');
237 response.headers.set('Content-Length', body.length);
238 // set etag before cache insertion
239 if (!response.headers.has('etag')) {
240 response.headers.set('etag', "" + pathKey);
241 }
242 // determine Cloudflare cache behavior
243 response.headers.set('Cache-Control', "max-age=" + options.cacheControl.edgeTTL);
244 event.waitUntil(cache.put(cacheKey, response.clone()));
245 response.headers.set('CF-Cache-Status', 'MISS');
246 }
247 _a.label = 5;
248 case 5:
249 response.headers.set('Content-Type', mimeType);
250 if (shouldSetBrowserCache) {
251 response.headers.set('Cache-Control', "max-age=" + options.cacheControl.browserTTL);
252 }
253 else {
254 response.headers.delete('Cache-Control');
255 }
256 return [2 /*return*/, response];
257 }
258 });
259}); };
260exports.getAssetFromKV = getAssetFromKV;